Giấy phép
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/3.0 or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
Lời tựa bởi Scott Chacon
Chào mừng bạn đến với phiên bản thứ hai của Pro Git. Phiên bản đầu tiên đã được xuất bản hơn bốn năm trước. Kể từ đó, rất nhiều thứ đã thay đổi nhưng nhiều điều quan trọng vẫn chưa thay đổi. Trong khi hầu hết các lệnh và khái niệm cốt lõi vẫn còn giá trị cho đến ngày nay vì nhóm nòng cốt Git khá tuyệt vời trong việc giữ cho mọi thứ tương thích ngược, đã có một số bổ sung và thay đổi đáng kể trong cộng đồng xung quanh Git. Phiên bản thứ hai của cuốn sách này nhằm giải quyết những thay đổi đó và cập nhật cuốn sách để nó có thể hữu ích hơn cho người dùng mới.
Khi tôi viết phiên bản đầu tiên, Git vẫn là một công cụ tương đối khó sử dụng và hầu như không được áp dụng cho hacker cốt lõi khó tính hơn. Nó bắt đầu đạt được sức hút trong một số cộng đồng nhất định, nhưng chưa đạt đến bất cứ nơi nào gần sự phổ biến như ngày nay. Kể từ đó, gần như mọi cộng đồng nguồn mở đều đã áp dụng nó. Git đã đạt được tiến bộ đáng kinh ngạc trên Windows, trong sự bùng nổ của các giao diện người dùng đồ họa cho nó cho tất cả các nền tảng, trong hỗ trợ IDE và trong việc sử dụng kinh doanh. Pro Git của bốn năm trước không biết gì về điều đó. Một trong những mục tiêu chính của phiên bản mới này là chạm vào tất cả những biên giới mới đó trong cộng đồng Git.
Cộng đồng Nguồn mở sử dụng Git cũng đã bùng nổ. Khi tôi ban đầu ngồi xuống để viết cuốn sách gần năm năm trước (tôi đã mất một thời gian để đưa phiên bản đầu tiên ra mắt), tôi vừa mới bắt đầu làm việc tại một công ty rất ít được biết đến đang phát triển một trang web lưu trữ Git có tên là GitHub. Vào thời điểm xuất bản, có lẽ có vài nghìn người sử dụng trang web và chỉ có bốn người chúng tôi làm việc trên đó. Khi tôi viết phần giới thiệu này, GitHub đang công bố dự án được lưu trữ thứ 10 triệu của chúng tôi, với gần 5 triệu tài khoản nhà phát triển đã đăng ký và hơn 230 nhân viên. Yêu hay ghét nó, GitHub đã thay đổi rất nhiều mảng lớn của cộng đồng Nguồn mở theo cách mà hầu như không thể hình dung được khi tôi ngồi xuống để viết phiên bản đầu tiên.
Tôi đã viết một phần nhỏ trong phiên bản gốc của Pro Git về GitHub như một ví dụ về Git được lưu trữ mà tôi chưa bao giờ cảm thấy thoải mái lắm. Tôi không thích lắm việc tôi đang viết những gì tôi cảm thấy về cơ bản là một tài nguyên cộng đồng và cũng nói về công ty của tôi trong đó. Mặc dù tôi vẫn không thích xung đột lợi ích đó, nhưng tầm quan trọng của GitHub trong cộng đồng Git là không thể tránh khỏi. Thay vì một ví dụ về lưu trữ Git, tôi đã quyết định biến phần đó của cuốn sách thành mô tả sâu hơn về GitHub là gì và cách sử dụng nó hiệu quả. Nếu bạn định học cách sử dụng Git thì việc biết cách sử dụng GitHub sẽ giúp bạn tham gia vào một cộng đồng lớn, điều này có giá trị bất kể bạn quyết định sử dụng máy chủ Git nào cho mã của riêng mình.
Thay đổi lớn khác trong thời gian kể từ lần xuất bản cuối cùng là sự phát triển và trỗi dậy của giao thức HTTP cho các giao dịch mạng Git. Hầu hết các ví dụ trong cuốn sách đã được thay đổi thành HTTP từ SSH vì nó đơn giản hơn nhiều.
Thật tuyệt vời khi xem Git phát triển trong vài năm qua từ một hệ thống kiểm soát phiên bản tương đối tối nghĩa để cơ bản thống trị kiểm soát phiên bản thương mại và nguồn mở. Tôi rất vui vì Pro Git đã làm rất tốt và cũng đã có thể trở thành một trong số ít những cuốn sách kỹ thuật trên thị trường vừa khá thành công vừa hoàn toàn là nguồn mở.
Tôi hy vọng bạn thích phiên bản cập nhật này của Pro Git.
Lời tựa bởi Ben Straub
Phiên bản đầu tiên của cuốn sách này là thứ đã khiến tôi say mê Git. Đây là phần giới thiệu của tôi về một phong cách tạo phần mềm cảm thấy tự nhiên hơn bất cứ thứ gì tôi từng thấy trước đây. Tôi đã là một nhà phát triển trong vài năm vào thời điểm đó, nhưng đây là ngã rẽ đúng đắn đã đưa tôi xuống một con đường thú vị hơn nhiều so với con đường tôi đang đi.
Bây giờ, nhiều năm sau, tôi là người đóng góp cho một triển khai Git lớn, tôi đã làm việc cho công ty lưu trữ Git lớn nhất và tôi đã đi khắp thế giới để dạy mọi người về Git. Khi Scott hỏi liệu tôi có quan tâm đến việc làm việc trên phiên bản thứ hai hay không, tôi thậm chí không cần phải suy nghĩ.
Thật vinh dự và đặc quyền khi được làm việc trên cuốn sách này. Tôi hy vọng nó giúp bạn nhiều như nó đã giúp tôi.
Lời đề tặng
Gửi vợ tôi, Becky, nếu không có cô ấy cuộc phiêu lưu này sẽ không bao giờ bắt đầu. — Ben
Phiên bản này được dành tặng cho các cô con gái của tôi. Gửi vợ tôi Jessica, người đã ủng hộ tôi trong suốt những năm qua và con gái Josephine của tôi, người sẽ ủng hộ tôi khi tôi quá già để biết chuyện gì đang xảy ra. — Scott
Những người đóng góp
Vì đây là một cuốn sách Nguồn mở, chúng tôi đã nhận được một số lỗi và thay đổi nội dung được đóng góp trong những năm qua. Dưới đây là tất cả những người đã đóng góp cho phiên bản tiếng Anh của Pro Git như một dự án nguồn mở. Cảm ơn mọi người đã giúp làm cho cuốn sách này tốt hơn cho mọi người.
Contributors as of 006562f4:
Giới thiệu
Bạn sắp dành vài giờ trong cuộc đời mình để đọc về Git. Hãy dành một phút để giải thích những gì chúng tôi dành cho bạn. Dưới đây là tóm tắt nhanh về mười chương và ba phụ lục của cuốn sách này.
Trong Chương 1, chúng tôi sẽ đề cập đến Hệ thống Kiểm soát Phiên bản (VCS) và những điều cơ bản về Git — không có nội dung kỹ thuật nào, chỉ là Git là gì, tại sao nó lại xuất hiện trong một vùng đất đầy rẫy các VCS, điều gì làm nên sự khác biệt của nó và tại sao rất nhiều người đang sử dụng nó. Sau đó, chúng tôi sẽ giải thích cách tải xuống Git và thiết lập nó lần đầu tiên nếu bạn chưa có nó trên hệ thống của mình.
Trong Chương 2, chúng tôi sẽ đi qua cách sử dụng Git cơ bản — cách sử dụng Git trong 80% trường hợp bạn sẽ gặp thường xuyên nhất. Sau khi đọc chương này, bạn sẽ có thể sao chép một kho lưu trữ, xem những gì đã xảy ra trong lịch sử của dự án, sửa đổi các tệp và đóng góp các thay đổi. Nếu cuốn sách tự bốc cháy vào thời điểm này, bạn đã khá hữu ích khi sử dụng Git trong thời gian bạn đi lấy một bản sao khác.
Chương 3 nói về mô hình phân nhánh trong Git, thường được mô tả là tính năng sát thủ của Git. Ở đây bạn sẽ tìm hiểu điều gì thực sự làm nên sự khác biệt của Git so với các gói khác. Khi bạn hoàn thành, bạn có thể cảm thấy cần phải dành một chút thời gian yên tĩnh để suy ngẫm về cách bạn đã sống trước khi phân nhánh Git là một phần của cuộc sống của bạn.
Chương 4 sẽ đề cập đến Git trên máy chủ. Chương này dành cho những bạn muốn thiết lập Git bên trong tổ chức của mình hoặc trên máy chủ cá nhân của riêng bạn để cộng tác. Chúng tôi cũng sẽ khám phá các tùy chọn được lưu trữ khác nhau nếu bạn muốn để người khác xử lý việc đó cho bạn.
Chương 5 sẽ đi sâu vào chi tiết đầy đủ về các quy trình làm việc phân tán khác nhau và cách thực hiện chúng với Git. Khi bạn hoàn thành chương này, bạn sẽ có thể làm việc thành thạo với nhiều kho lưu trữ từ xa, sử dụng Git qua email và khéo léo tung hứng nhiều nhánh từ xa và các bản vá được đóng góp.
Chương 6 đề cập sâu về dịch vụ lưu trữ GitHub và công cụ. Chúng tôi đề cập đến việc đăng ký và quản lý tài khoản, tạo và sử dụng kho lưu trữ Git, quy trình làm việc chung để đóng góp cho các dự án và chấp nhận đóng góp cho dự án của bạn, giao diện lập trình của GitHub và rất nhiều mẹo nhỏ để làm cho cuộc sống của bạn dễ dàng hơn nói chung.
Chương 7 nói về các lệnh Git nâng cao. Ở đây bạn sẽ tìm hiểu về các chủ đề như làm chủ lệnh 'reset' đáng sợ, sử dụng tìm kiếm nhị phân để xác định lỗi, chỉnh sửa lịch sử, lựa chọn sửa đổi chi tiết và hơn thế nữa. Chương này sẽ hoàn thiện kiến thức của bạn về Git để bạn thực sự là một bậc thầy.
Chương 8 nói về việc cấu hình môi trường Git tùy chỉnh của bạn. Điều này bao gồm thiết lập các tập lệnh hook để thực thi hoặc khuyến khích các chính sách tùy chỉnh và sử dụng cài đặt cấu hình môi trường để bạn có thể làm việc theo cách bạn muốn. Chúng tôi cũng sẽ đề cập đến việc xây dựng bộ tập lệnh của riêng bạn để thực thi chính sách cam kết tùy chỉnh.
Chương 9 đề cập đến Git và các VCS khác. Điều này bao gồm sử dụng Git trong thế giới Subversion (SVN) và chuyển đổi các dự án từ các VCS khác sang Git. Rất nhiều tổ chức vẫn sử dụng SVN và không có ý định thay đổi, nhưng đến thời điểm này, bạn sẽ học được sức mạnh đáng kinh ngạc của Git — và chương này chỉ cho bạn cách đối phó nếu bạn vẫn phải sử dụng máy chủ SVN. Chúng tôi cũng đề cập đến cách nhập các dự án từ một số hệ thống khác nhau trong trường hợp bạn thuyết phục được mọi người thực hiện bước nhảy vọt.
Chương 10 đi sâu vào chiều sâu tối tăm nhưng đẹp đẽ của nội bộ Git. Bây giờ bạn đã biết tất cả về Git và có thể sử dụng nó với sức mạnh và sự duyên dáng, bạn có thể chuyển sang thảo luận về cách Git lưu trữ các đối tượng của nó, mô hình đối tượng là gì, chi tiết về packfiles, giao thức máy chủ và hơn thế nữa. Trong suốt cuốn sách, chúng tôi sẽ tham khảo các phần của chương này trong trường hợp bạn cảm thấy muốn đi sâu vào thời điểm đó; nhưng nếu bạn giống chúng tôi và muốn đi sâu vào các chi tiết kỹ thuật, bạn có thể muốn đọc Chương 10 trước. Chúng tôi để điều đó tùy thuộc vào bạn.
Trong Phụ lục A, chúng tôi xem xét một số ví dụ về việc sử dụng Git trong các môi trường cụ thể khác nhau. Chúng tôi đề cập đến một số GUI và môi trường lập trình IDE khác nhau mà bạn có thể muốn sử dụng Git trong đó và những gì có sẵn cho bạn. Nếu bạn quan tâm đến tổng quan về việc sử dụng Git trong shell, IDE hoặc trình soạn thảo văn bản của mình, hãy xem tại đây.
Trong Phụ lục B, chúng tôi khám phá việc viết kịch bản và mở rộng Git thông qua các công cụ như libgit2 và JGit. Nếu bạn quan tâm đến việc viết các công cụ tùy chỉnh phức tạp và nhanh chóng và cần quyền truy cập Git cấp thấp, đây là nơi bạn có thể xem bối cảnh đó trông như thế nào.
Cuối cùng, trong Phụ lục C, chúng tôi đi qua tất cả các lệnh Git chính từng cái một và xem xét nơi trong cuốn sách chúng tôi đã đề cập đến chúng và những gì chúng tôi đã làm với chúng. Nếu bạn muốn biết nơi trong cuốn sách chúng tôi đã sử dụng bất kỳ lệnh Git cụ thể nào, bạn có thể tra cứu tại đây.
Hãy bắt đầu nào.
Bắt đầu
Chương này giới thiệu cách bắt đầu sử dụng Git. Chúng tôi sẽ bắt đầu bằng việc giải thích một số kiến thức nền về các công cụ quản lý phiên bản, sau đó chuyển sang cách cài đặt Git trên hệ thống của bạn và cuối cùng là cách thiết lập để bắt đầu làm việc với Git. Cuối chương này bạn sẽ hiểu lý do tồn tại của Git, vì sao nên sử dụng nó và sẽ có Git hoạt động trên máy với cấu hình danh tính cá nhân của bạn.
Về Kiểm soát Phiên bản
“Kiểm soát phiên bản” là gì, và tại sao bạn nên quan tâm? Kiểm soát phiên bản là một hệ thống ghi lại các thay đổi đối với một tệp hoặc tập hợp các tệp theo thời gian để bạn có thể gọi lại các phiên bản cụ thể sau này. Đối với các ví dụ trong cuốn sách này, bạn sẽ sử dụng mã nguồn phần mềm làm các tệp được kiểm soát phiên bản, mặc dù trong thực tế, bạn có thể làm điều này với gần như bất kỳ loại tệp nào trên máy tính.
Nếu bạn là một nhà thiết kế đồ họa hoặc web và muốn giữ mọi phiên bản của một hình ảnh hoặc bố cục (điều mà bạn chắc chắn sẽ muốn), một Hệ thống Kiểm soát Phiên bản (VCS) là một điều rất khôn ngoan để sử dụng. Nó cho phép bạn hoàn tác các tệp đã chọn trở lại trạng thái trước đó, hoàn tác toàn bộ dự án trở lại trạng thái trước đó, so sánh các thay đổi theo thời gian, xem ai đã sửa đổi lần cuối cùng một cái gì đó có thể gây ra sự cố, ai đã giới thiệu một vấn đề và khi nào, và hơn thế nữa. Sử dụng VCS cũng thường có nghĩa là nếu bạn làm hỏng mọi thứ hoặc mất tệp, bạn có thể dễ dàng khôi phục. Ngoài ra, bạn nhận được tất cả những điều này với rất ít chi phí.
Hệ thống Kiểm soát Phiên bản Cục bộ
Phương pháp kiểm soát phiên bản được lựa chọn của nhiều người là sao chép các tệp vào một thư mục khác (có thể là một thư mục có dấu thời gian, nếu họ thông minh). Cách tiếp cận này rất phổ biến vì nó rất đơn giản, nhưng nó cũng cực kỳ dễ bị lỗi. Thật dễ dàng để quên bạn đang ở thư mục nào và vô tình ghi vào sai tệp hoặc sao chép qua các tệp bạn không có ý định.
Để giải quyết vấn đề này, các lập trình viên từ lâu đã phát triển các VCS cục bộ có một cơ sở dữ liệu đơn giản giữ tất cả các thay đổi đối với các tệp dưới sự kiểm soát sửa đổi.
Một trong những công cụ VCS phổ biến nhất là một hệ thống có tên là RCS, vẫn được phân phối với nhiều máy tính ngày nay. RCS hoạt động bằng cách giữ các bộ bản vá (nghĩa là sự khác biệt giữa các tệp) ở định dạng đặc biệt trên đĩa; sau đó nó có thể tạo lại bất kỳ tệp nào trông như thế nào tại bất kỳ thời điểm nào bằng cách cộng tất cả các bản vá.
Hệ thống Kiểm soát Phiên bản Tập trung
Vấn đề lớn tiếp theo mà mọi người gặp phải là họ cần cộng tác với các nhà phát triển trên các hệ thống khác. Để giải quyết vấn đề này, Hệ thống Kiểm soát Phiên bản Tập trung (CVCSs) đã được phát triển. Các hệ thống này (như CVS, Subversion và Perforce) có một máy chủ duy nhất chứa tất cả các tệp được tạo phiên bản và một số máy khách kiểm xuất các tệp từ nơi trung tâm đó. Trong nhiều năm, đây đã là tiêu chuẩn cho kiểm soát phiên bản.
Thiết lập này cung cấp nhiều lợi thế, đặc biệt là so với các VCS cục bộ. Ví dụ, mọi người đều biết ở một mức độ nhất định những gì người khác trong dự án đang làm. Quản trị viên có quyền kiểm soát chi tiết đối với ai có thể làm gì và quản trị một CVCS dễ dàng hơn nhiều so với việc xử lý các cơ sở dữ liệu cục bộ trên mọi máy khách.
Tuy nhiên, thiết lập này cũng có một số nhược điểm nghiêm trọng. Rõ ràng nhất là điểm lỗi duy nhất mà máy chủ tập trung đại diện. Nếu máy chủ đó bị hỏng trong một giờ, thì trong giờ đó, không ai có thể cộng tác hoặc lưu các thay đổi đã được tạo phiên bản vào bất cứ thứ gì họ đang làm việc. Nếu đĩa cứng mà cơ sở dữ liệu trung tâm nằm trên đó bị hỏng và các bản sao lưu thích hợp chưa được giữ, bạn sẽ mất hoàn toàn mọi thứ — toàn bộ lịch sử của dự án ngoại trừ bất kỳ ảnh chụp nhanh đơn lẻ nào mà mọi người tình cờ có trên máy cục bộ của họ. Các VCS cục bộ cũng gặp vấn đề tương tự — bất cứ khi nào bạn có toàn bộ lịch sử của dự án ở một nơi duy nhất, bạn có nguy cơ mất tất cả.
Hệ thống Kiểm soát Phiên bản Phân tán
Đây là nơi Hệ thống Kiểm soát Phiên bản Phân tán (DVCSs) bước vào. Trong một DVCS (như Git, Mercurial hoặc Darcs), các máy khách không chỉ kiểm xuất ảnh chụp nhanh mới nhất của các tệp; thay vào đó, họ phản chiếu đầy đủ kho lưu trữ, bao gồm cả lịch sử đầy đủ của nó. Do đó, nếu bất kỳ máy chủ nào bị chết và các hệ thống này đang cộng tác qua máy chủ đó, bất kỳ kho lưu trữ máy khách nào cũng có thể được sao chép lại lên máy chủ để khôi phục nó. Mỗi bản sao thực sự là một bản sao lưu đầy đủ của tất cả dữ liệu.
Hơn nữa, nhiều hệ thống trong số này xử lý khá tốt việc có một số kho lưu trữ từ xa mà họ có thể làm việc cùng, vì vậy bạn có thể cộng tác với các nhóm người khác nhau theo những cách khác nhau đồng thời trong cùng một dự án. Điều này cho phép bạn thiết lập một số loại quy trình làm việc không thể thực hiện được trong các hệ thống tập trung, chẳng hạn như các mô hình phân cấp.
Lịch sử Ngắn gọn về Git
Như với nhiều điều tuyệt vời trong cuộc sống, Git bắt đầu với một chút phá hủy sáng tạo và tranh cãi gay gắt.
Nhân Linux là một dự án phần mềm nguồn mở có phạm vi khá lớn. Trong những năm đầu bảo trì nhân Linux (1991–2002), các thay đổi đối với phần mềm được truyền xung quanh dưới dạng các bản vá và tệp lưu trữ. Năm 2002, dự án nhân Linux bắt đầu sử dụng một DVCS độc quyền có tên là BitKeeper.
Năm 2005, mối quan hệ giữa cộng đồng phát triển nhân Linux và công ty thương mại phát triển BitKeeper đã đổ vỡ, và trạng thái miễn phí của công cụ đã bị thu hồi. Điều này đã thúc đẩy cộng đồng phát triển Linux (và đặc biệt là Linus Torvalds, người tạo ra Linux) phát triển công cụ riêng của họ dựa trên một số bài học họ học được khi sử dụng BitKeeper. Một số mục tiêu của hệ thống mới như sau:
-
Tốc độ
-
Thiết kế đơn giản
-
Hỗ trợ mạnh mẽ cho phát triển phi tuyến tính (hàng nghìn nhánh song song)
-
Phân tán hoàn toàn
-
Có khả năng xử lý các dự án lớn như nhân Linux một cách hiệu quả (tốc độ và kích thước dữ liệu)
Kể từ khi ra đời vào năm 2005, Git đã phát triển và trưởng thành để dễ sử dụng nhưng vẫn giữ được những phẩm chất ban đầu này. Nó nhanh đến kinh ngạc, rất hiệu quả với các dự án lớn và có một hệ thống phân nhánh đáng kinh ngạc cho phát triển phi tuyến tính (xem Nhánh trong Git).
Git là gì?
Vậy, Git là gì một cách ngắn gọn? Đây là một phần quan trọng để tiếp thu, bởi vì nếu bạn hiểu Git là gì và các nguyên tắc cơ bản về cách nó hoạt động, thì việc sử dụng Git hiệu quả có lẽ sẽ dễ dàng hơn nhiều đối với bạn. Khi bạn học Git, hãy cố gắng xóa sạch tâm trí của bạn khỏi những điều bạn có thể biết về các VCS khác, chẳng hạn như CVS, Subversion hoặc Perforce — làm như vậy sẽ giúp bạn tránh sự nhầm lẫn tinh tế khi sử dụng công cụ. Mặc dù giao diện người dùng của Git khá giống với các VCS khác này, Git lưu trữ và suy nghĩ về thông tin theo một cách rất khác, và việc hiểu những khác biệt này sẽ giúp bạn tránh bị nhầm lẫn khi sử dụng nó.
Ảnh chụp nhanh, Không phải Sự khác biệt
Sự khác biệt chính giữa Git và bất kỳ VCS nào khác (Subversion và bạn bè bao gồm) là cách Git suy nghĩ về dữ liệu của nó. Về mặt khái niệm, hầu hết các hệ thống khác lưu trữ thông tin dưới dạng danh sách các thay đổi dựa trên tệp. Các hệ thống khác này (CVS, Subversion, Perforce, v.v.) nghĩ về thông tin mà chúng lưu trữ như một tập hợp các tệp và các thay đổi được thực hiện đối với mỗi tệp theo thời gian (điều này thường được mô tả là kiểm soát phiên bản dựa trên delta).
Git không nghĩ hoặc lưu trữ dữ liệu của nó theo cách này. Thay vào đó, Git nghĩ về dữ liệu của nó giống như một loạt các ảnh chụp nhanh của một hệ thống tệp thu nhỏ. Với Git, mỗi khi bạn cam kết, hoặc lưu trạng thái của dự án của bạn, Git về cơ bản chụp một bức ảnh về tất cả các tệp của bạn trông như thế nào tại thời điểm đó và lưu trữ một tham chiếu đến ảnh chụp nhanh đó. Để hiệu quả, nếu các tệp không thay đổi, Git không lưu trữ lại tệp, chỉ là một liên kết đến tệp giống hệt trước đó mà nó đã lưu trữ. Git nghĩ về dữ liệu của nó giống như một luồng ảnh chụp nhanh.
Đây là một sự khác biệt quan trọng giữa Git và gần như tất cả các VCS khác. Nó làm cho Git xem xét lại gần như mọi khía cạnh của kiểm soát phiên bản mà hầu hết các hệ thống khác sao chép từ thế hệ trước. Điều này làm cho Git giống như một hệ thống tệp nhỏ với một số công cụ cực kỳ mạnh mẽ được xây dựng trên đó, thay vì chỉ đơn giản là một VCS. Chúng tôi sẽ khám phá một số lợi ích bạn đạt được bằng cách suy nghĩ về dữ liệu của bạn theo cách này khi chúng tôi đề cập đến phân nhánh Git trong Nhánh trong Git.
Gần như Mọi Thao tác Đều Cục bộ
Hầu hết các thao tác trong Git chỉ cần các tệp và tài nguyên cục bộ để hoạt động — thường không cần thông tin từ một máy tính khác trên mạng của bạn. Nếu bạn quen với CVCS nơi hầu hết các thao tác có chi phí độ trễ mạng đó, khía cạnh này của Git sẽ khiến bạn nghĩ rằng các vị thần tốc độ đã ban phước cho Git với sức mạnh siêu nhiên. Bởi vì bạn có toàn bộ lịch sử của dự án ngay tại đó trên đĩa cục bộ của bạn, hầu hết các thao tác dường như gần như tức thì.
Ví dụ, để duyệt lịch sử của dự án, Git không cần phải ra ngoài máy chủ để lấy lịch sử và hiển thị nó cho bạn — nó chỉ đơn giản là đọc nó trực tiếp từ cơ sở dữ liệu cục bộ của bạn. Điều này có nghĩa là bạn thấy lịch sử dự án gần như ngay lập tức. Nếu bạn muốn xem các thay đổi được giới thiệu giữa phiên bản hiện tại của một tệp và tệp một tháng trước, Git có thể tra cứu tệp một tháng trước và thực hiện tính toán sự khác biệt cục bộ, thay vì phải yêu cầu máy chủ từ xa làm điều đó hoặc kéo một phiên bản cũ hơn của tệp từ máy chủ từ xa để làm điều đó cục bộ.
Điều này cũng có nghĩa là có rất ít điều bạn không thể làm nếu bạn ngoại tuyến hoặc tắt VPN. Nếu bạn lên máy bay hoặc tàu hỏa và muốn làm một chút việc, bạn có thể cam kết vui vẻ (vào bản sao cục bộ của bạn, nhớ chứ?) cho đến khi bạn có kết nối mạng để tải lên. Nếu bạn về nhà và không thể làm cho ứng dụng khách VPN của bạn hoạt động đúng, bạn vẫn có thể làm việc. Trong nhiều hệ thống khác, làm như vậy là không thể hoặc đau đớn. Ví dụ, trong Perforce, bạn không thể làm được nhiều khi bạn không được kết nối với máy chủ; trong Subversion và CVS, bạn có thể chỉnh sửa tệp, nhưng bạn không thể cam kết các thay đổi vào cơ sở dữ liệu của mình (vì cơ sở dữ liệu của bạn ngoại tuyến). Điều này có thể không có vẻ như một vấn đề lớn, nhưng bạn có thể ngạc nhiên về sự khác biệt lớn mà nó có thể tạo ra.
Git Có Tính Toàn vẹn
Mọi thứ trong Git được kiểm tra tổng kiểm tra trước khi nó được lưu trữ và sau đó được tham chiếu bởi tổng kiểm tra đó. Điều này có nghĩa là không thể thay đổi nội dung của bất kỳ tệp hoặc thư mục nào mà Git không biết về nó. Chức năng này được tích hợp vào Git ở các cấp thấp nhất và là một phần không thể thiếu trong triết lý của nó. Bạn không thể mất thông tin trong quá trình truyền hoặc bị hỏng tệp mà Git không thể phát hiện nó.
Cơ chế mà Git sử dụng cho tổng kiểm tra này được gọi là băm SHA-1. Đây là một chuỗi 40 ký tự được tạo thành từ các ký tự thập lục phân (0–9 và a–f) và được tính toán dựa trên nội dung của một tệp hoặc cấu trúc thư mục trong Git. Băm SHA-1 trông giống như thế này:
24b9da6552252987aa493b52f8696cd6d3b00373
Bạn sẽ thấy các giá trị băm này ở khắp mọi nơi trong Git vì nó sử dụng chúng rất nhiều. Trên thực tế, Git lưu trữ mọi thứ trong cơ sở dữ liệu của nó không phải bằng tên tệp mà bằng giá trị băm của nội dung của nó.
Git Thường Chỉ Thêm Dữ liệu
Khi bạn thực hiện các hành động trong Git, gần như tất cả chúng chỉ thêm dữ liệu vào cơ sở dữ liệu Git. Thật khó để làm cho hệ thống làm bất cứ điều gì không thể hoàn tác hoặc làm cho nó xóa dữ liệu theo bất kỳ cách nào. Như với bất kỳ VCS nào, bạn có thể mất hoặc làm hỏng các thay đổi bạn chưa cam kết, nhưng sau khi bạn cam kết một ảnh chụp nhanh vào Git, rất khó để mất, đặc biệt nếu bạn thường xuyên đẩy cơ sở dữ liệu của mình sang một kho lưu trữ khác.
Điều này làm cho việc sử dụng Git trở thành một niềm vui vì chúng ta biết chúng ta có thể thử nghiệm mà không có nguy cơ làm hỏng mọi thứ nghiêm trọng. Để xem xét sâu hơn về cách Git lưu trữ dữ liệu của nó và cách bạn có thể khôi phục dữ liệu có vẻ bị mất, hãy xem Hoàn tác Mọi thứ.
Ba Trạng thái
Hãy chú ý bây giờ — đây là điều chính cần nhớ về Git nếu bạn muốn phần còn lại của quá trình học tập của bạn diễn ra suôn sẻ. Git có ba trạng thái chính mà các tệp của bạn có thể cư trú trong: modified (đã sửa đổi), staged (đã tổ chức), và committed (đã cam kết):
-
Modified có nghĩa là bạn đã thay đổi tệp nhưng chưa cam kết nó vào cơ sở dữ liệu của bạn.
-
Staged có nghĩa là bạn đã đánh dấu một tệp đã sửa đổi ở phiên bản hiện tại của nó để đưa vào ảnh chụp nhanh cam kết tiếp theo của bạn.
-
Committed có nghĩa là dữ liệu được lưu trữ an toàn trong cơ sở dữ liệu cục bộ của bạn.
Điều này dẫn chúng ta đến ba phần chính của một dự án Git: cây làm việc, khu vực tổ chức và thư mục Git.
Cây làm việc là một lần kiểm xuất duy nhất của một phiên bản của dự án. Các tệp này được kéo ra khỏi cơ sở dữ liệu nén trong thư mục Git và được đặt trên đĩa để bạn sử dụng hoặc sửa đổi.
Khu vực tổ chức là một tệp, thường được chứa trong thư mục Git của bạn, lưu trữ thông tin về những gì sẽ đi vào cam kết tiếp theo của bạn. Tên kỹ thuật của nó trong thuật ngữ Git là “index”, nhưng cụm từ “staging area” cũng hoạt động tốt.
Thư mục Git là nơi Git lưu trữ siêu dữ liệu và cơ sở dữ liệu đối tượng cho dự án của bạn. Đây là phần quan trọng nhất của Git, và đó là những gì được sao chép khi bạn sao chép một kho lưu trữ từ một máy tính khác.
Quy trình làm việc Git cơ bản diễn ra như thế này:
-
Bạn sửa đổi các tệp trong cây làm việc của bạn.
-
Bạn chọn lọc tổ chức chỉ những thay đổi bạn muốn là một phần của cam kết tiếp theo của bạn, điều này chỉ thêm những thay đổi đó vào khu vực tổ chức.
-
Bạn thực hiện một cam kết, lấy các tệp như chúng đang ở trong khu vực tổ chức và lưu trữ ảnh chụp nhanh đó vĩnh viễn vào thư mục Git của bạn.
Nếu một phiên bản cụ thể của một tệp nằm trong thư mục Git, nó được coi là committed. Nếu nó đã được sửa đổi và đã được thêm vào khu vực tổ chức, nó là staged. Và nếu nó đã được thay đổi kể từ khi nó được kiểm xuất nhưng chưa được tổ chức, nó là modified. Trong Các khái niệm cơ bản về Git, bạn sẽ tìm hiểu thêm về các trạng thái này và cách bạn có thể tận dụng chúng hoặc bỏ qua phần staged hoàn toàn.
Dòng Lệnh
Có rất nhiều cách khác nhau để sử dụng Git. Có các công cụ dòng lệnh ban đầu, và có nhiều giao diện người dùng đồ họa với các khả năng khác nhau. Đối với cuốn sách này, chúng tôi sẽ sử dụng Git trên dòng lệnh. Thứ nhất, dòng lệnh là nơi duy nhất bạn có thể chạy tất cả các lệnh Git — hầu hết các GUI chỉ triển khai một tập hợp con một phần của chức năng Git để đơn giản hóa. Nếu bạn biết cách chạy phiên bản dòng lệnh, bạn có thể cũng có thể tìm ra cách chạy phiên bản GUI, trong khi điều ngược lại không nhất thiết đúng. Ngoài ra, trong khi lựa chọn ứng dụng khách đồ họa của bạn là vấn đề sở thích cá nhân, tất cả người dùng sẽ có các công cụ dòng lệnh được cài đặt và có sẵn.
Vì vậy, chúng tôi sẽ mong đợi bạn biết cách mở Terminal trong macOS hoặc Command Prompt hoặc PowerShell trong Windows. Nếu bạn không biết chúng tôi đang nói về điều gì ở đây, bạn có thể cần dừng lại và nghiên cứu điều đó nhanh chóng để bạn có thể làm theo phần còn lại của các ví dụ và mô tả trong cuốn sách này.
Cài đặt Git
Trước khi bạn bắt đầu sử dụng Git, bạn phải làm cho nó có sẵn trên máy tính của mình. Ngay cả khi nó đã được cài đặt, có lẽ là một ý tưởng tốt để cập nhật lên phiên bản mới nhất. Bạn có thể cài đặt nó dưới dạng gói hoặc thông qua trình cài đặt khác, hoặc tải xuống mã nguồn và biên dịch nó cho riêng bạn.
|
Cuốn sách này được viết bằng Git phiên bản 2. Vì Git khá xuất sắc trong việc bảo tồn khả năng tương thích ngược, bất kỳ phiên bản gần đây nào cũng sẽ hoạt động tốt. Mặc dù hầu hết các lệnh chúng tôi sử dụng sẽ hoạt động ngay cả trong các phiên bản Git cổ xưa, một số trong số chúng có thể không hoạt động hoặc có thể hoạt động hơi khác một chút. |
Cài đặt trên Linux
Nếu bạn muốn cài đặt các công cụ Git cơ bản trên Linux thông qua trình cài đặt nhị phân, bạn thường có thể làm như vậy thông qua công cụ quản lý gói đi kèm với bản phân phối của bạn.
Nếu bạn đang sử dụng Fedora (hoặc bất kỳ bản phân phối dựa trên RPM liên quan chặt chẽ nào, chẳng hạn như RHEL hoặc CentOS), bạn có thể sử dụng dnf:
$ sudo dnf install git-all
Nếu bạn đang sử dụng bản phân phối dựa trên Debian, chẳng hạn như Ubuntu, hãy thử apt:
$ sudo apt install git-all
Để biết thêm tùy chọn, có hướng dẫn cài đặt trên một số bản phân phối Unix khác nhau trên trang web Git, tại https://git-scm.com/download/linux.
Cài đặt trên macOS
Có một số cách để cài đặt Git trên macOS.
Cách dễ nhất có lẽ là cài đặt Xcode Command Line Tools.
Trên Mavericks (10.9) trở lên, bạn có thể làm điều này đơn giản bằng cách cố gắng chạy git từ Terminal lần đầu tiên.
$ git --version
Nếu bạn chưa cài đặt nó, nó sẽ nhắc bạn cài đặt nó.
Nếu bạn muốn một phiên bản cập nhật hơn, bạn cũng có thể cài đặt nó thông qua trình cài đặt nhị phân. Trình cài đặt Git macOS được duy trì và có sẵn để tải xuống tại trang web Git, tại https://git-scm.com/download/mac.
Cài đặt trên Windows
Cũng có một vài cách để cài đặt Git trên Windows. Bản dựng chính thức nhất có sẵn để tải xuống trên trang web Git. Chỉ cần truy cập https://git-scm.com/download/win và quá trình tải xuống sẽ bắt đầu tự động. Lưu ý rằng đây là một dự án có tên là Git for Windows, tách biệt với chính Git; để biết thêm thông tin về nó, hãy truy cập https://gitforwindows.org.
Để có được cài đặt tự động, bạn có thể sử dụng gói Git Chocolatey. Lưu ý rằng gói Chocolatey được cộng đồng duy trì.
Cài đặt từ Mã nguồn
Một số người có thể thấy hữu ích khi cài đặt Git từ mã nguồn, bởi vì bạn sẽ nhận được phiên bản gần đây nhất. Các trình cài đặt nhị phân có xu hướng hơi chậm, mặc dù khi Git đã trưởng thành trong những năm gần đây, điều này đã tạo ra ít sự khác biệt hơn.
Nếu bạn muốn cài đặt Git từ mã nguồn, bạn cần có các thư viện sau mà Git phụ thuộc vào: autotools, curl, zlib, openssl, expat và libiconv.
Ví dụ, nếu bạn đang ở trên một hệ thống có dnf (chẳng hạn như Fedora) hoặc apt-get (chẳng hạn như một hệ thống dựa trên Debian), bạn có thể sử dụng một trong các lệnh này để cài đặt các phụ thuộc tối thiểu để biên dịch và cài đặt các tệp nhị phân Git:
$ sudo dnf install dh-autoreconf curl-devel expat-devel gettext-devel \
openssl-devel perl-devel zlib-devel
$ sudo apt-get install dh-autoreconf libcurl4-gnutls-dev libexpat1-dev \
gettext libz-dev libssl-dev
Để có thể thêm tài liệu ở các định dạng khác nhau (doc, html, info), các phụ thuộc bổ sung sau là bắt buộc:
$ sudo dnf install asciidoc xmlto docbook2X
$ sudo apt-get install asciidoc xmlto docbook2x
|
Người dùng RHEL và các dẫn xuất RHEL như CentOS và Scientific Linux sẽ phải bật kho lưu trữ EPEL để tải xuống gói |
Nếu bạn đang sử dụng bản phân phối dựa trên Debian (Debian/Ubuntu/Ubuntu-derivatives), bạn cũng cần gói install-info:
$ sudo apt-get install install-info
Nếu bạn đang sử dụng bản phân phối dựa trên RPM (Fedora/RHEL/RHEL-derivatives), bạn cũng cần gói getopt (đã được cài đặt trên bản phân phối dựa trên Debian):
$ sudo dnf install getopt
Ngoài ra, nếu bạn đang sử dụng Fedora/RHEL/RHEL-derivatives, bạn cần làm điều này:
$ sudo ln -s /usr/bin/db2x_docbook2texi /usr/bin/docbook2x-texi
do sự khác biệt về tên nhị phân.
Khi bạn có tất cả các phụ thuộc cần thiết, bạn có thể tiếp tục và lấy tarball phát hành được gắn thẻ mới nhất từ một số nơi. Bạn có thể lấy nó thông qua trang web kernel.org, tại https://www.kernel.org/pub/software/scm/git, hoặc bản sao trên trang web GitHub, tại https://github.com/git/git/tags. Nói chung, phiên bản mới nhất là gì sẽ rõ ràng hơn một chút trên trang GitHub, nhưng trang kernel.org cũng có chữ ký phát hành nếu bạn muốn xác minh tải xuống của mình.
Sau đó, biên dịch và cài đặt:
$ tar -zxf git-2.8.0.tar.gz
$ cd git-2.8.0
$ make configure
$ ./configure --prefix=/usr
$ make all doc info
$ sudo make install install-doc install-html install-info
Sau khi điều này hoàn tất, bạn cũng có thể lấy Git thông qua chính Git để cập nhật:
$ git clone https://git.kernel.org/pub/scm/git/git.git
Thiết lập Git Lần đầu
Bây giờ bạn đã có Git trên hệ thống của mình, bạn sẽ muốn làm một vài điều để tùy chỉnh môi trường Git của mình. Bạn chỉ nên làm những điều này một lần trên bất kỳ máy tính nào; chúng sẽ tồn tại giữa các lần nâng cấp. Bạn cũng có thể thay đổi chúng bất cứ lúc nào bằng cách chạy lại các lệnh.
Git đi kèm với một công cụ gọi là git config cho phép bạn nhận và đặt các biến cấu hình kiểm soát tất cả các khía cạnh về cách Git trông và hoạt động.
Các biến này có thể được lưu trữ ở ba nơi khác nhau:
-
Tệp
[path]/etc/gitconfig: Chứa các giá trị được áp dụng cho mọi người dùng trên hệ thống và tất cả các kho lưu trữ của họ. Nếu bạn chuyển tùy chọn--systemchogit config, nó sẽ đọc và ghi từ tệp này cụ thể. Bởi vì đây là tệp cấu hình hệ thống, bạn sẽ cần quyền quản trị hoặc siêu người dùng để thực hiện thay đổi đối với nó. -
Tệp
~/.gitconfighoặc~/.config/git/config: Các giá trị cụ thể cho cá nhân bạn, người dùng. Bạn có thể làm cho Git đọc và ghi vào tệp này cụ thể bằng cách chuyển tùy chọn--global, và điều này ảnh hưởng đến tất cả các kho lưu trữ bạn làm việc trên hệ thống của mình. -
Tệp
configtrong thư mục Git (nghĩa là.git/config) của bất kỳ kho lưu trữ nào bạn đang sử dụng: Cụ thể cho kho lưu trữ duy nhất đó. Bạn có thể buộc Git đọc từ và ghi vào tệp này bằng tùy chọn--local, nhưng thực tế đó là mặc định. Không ngạc nhiên, bạn cần phải ở đâu đó trong một kho lưu trữ Git để tùy chọn này hoạt động bình thường.
Mỗi cấp độ ghi đè các giá trị ở cấp độ trước đó, vì vậy các giá trị trong .git/config thắng các giá trị trong [path]/etc/gitconfig.
Trên hệ thống Windows, Git tìm kiếm tệp .gitconfig trong thư mục $HOME (C:\Users\$USER cho hầu hết mọi người).
Nó cũng vẫn tìm kiếm [path]/etc/gitconfig, mặc dù nó tương đối với gốc MSys, là bất cứ nơi nào bạn quyết định cài đặt Git trên hệ thống Windows của mình khi bạn chạy trình cài đặt.
Nếu bạn đang sử dụng phiên bản 2.x trở lên của Git cho Windows, cũng có một tệp cấu hình cấp hệ thống tại C:\Documents and Settings\All Users\Application Data\Git\config trên Windows XP, và trong C:\ProgramData\Git\config trên Windows Vista và mới hơn.
Tệp cấu hình này chỉ có thể được thay đổi bởi git config -f <file> với tư cách là quản trị viên.
Bạn có thể xem tất cả các cài đặt của mình và chúng đến từ đâu bằng cách sử dụng:
$ git config --list --show-origin
Danh tính của Bạn
Điều đầu tiên bạn nên làm khi cài đặt Git là đặt tên người dùng và địa chỉ email của bạn. Điều này quan trọng vì mọi cam kết Git đều sử dụng thông tin này và nó được gắn chặt vào các cam kết bạn bắt đầu tạo:
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
Một lần nữa, bạn chỉ cần làm điều này một lần nếu bạn chuyển tùy chọn --global, bởi vì sau đó Git sẽ luôn sử dụng thông tin đó cho người dùng của bạn trên hệ thống đó.
Nếu bạn muốn ghi đè điều này bằng một tên hoặc địa chỉ email khác cho các dự án cụ thể, bạn có thể chạy lệnh mà không có tùy chọn --global khi bạn đang ở trong dự án đó.
Nhiều công cụ GUI sẽ giúp bạn làm điều này khi bạn chạy chúng lần đầu tiên.
Trình soạn thảo của Bạn
Bây giờ danh tính của bạn đã được thiết lập, bạn có thể cấu hình trình soạn thảo văn bản mặc định sẽ được sử dụng khi Git cần bạn nhập tin nhắn. Nếu không được cấu hình, Git sử dụng trình soạn thảo mặc định của hệ thống của bạn.
Nếu bạn muốn sử dụng một trình soạn thảo văn bản khác, chẳng hạn như Emacs, bạn có thể làm như sau:
$ git config --global core.editor emacs
Trên hệ thống Windows, nếu bạn muốn sử dụng một trình soạn thảo văn bản khác, bạn phải chỉ định đường dẫn đầy đủ đến tệp thực thi của nó. Điều này có thể khác nhau tùy thuộc vào cách trình soạn thảo của bạn được đóng gói.
Trong trường hợp của Notepad++, một trình soạn thảo lập trình phổ biến, bạn có thể muốn sử dụng phiên bản 32-bit, vì tại thời điểm viết bài, phiên bản 64-bit không hỗ trợ tất cả các plug-in. Nếu bạn đang ở trên hệ thống Windows 32-bit, hoặc bạn có một trình soạn thảo 64-bit trên hệ thống 64-bit, bạn sẽ nhập một cái gì đó như thế này:
$ git config --global core.editor "'C:/Program Files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin"
|
Vim, Emacs và Notepad++ là các trình soạn thảo văn bản phổ biến thường được sử dụng bởi các nhà phát triển trên các hệ thống dựa trên Unix như Linux và macOS hoặc một hệ thống Windows. Nếu bạn đang sử dụng một trình soạn thảo khác, hoặc một phiên bản 32-bit, vui lòng tìm hướng dẫn cụ thể về cách thiết lập trình soạn thảo yêu thích của bạn với Git trong [ch_core_editor]. |
|
Bạn có thể thấy, nếu bạn không thiết lập trình soạn thảo của mình như thế này, bạn sẽ rơi vào trạng thái thực sự khó hiểu khi Git cố gắng khởi chạy nó. Một ví dụ trên hệ thống Windows có thể bao gồm một hoạt động Git bị chấm dứt sớm trong quá trình chỉnh sửa do Git khởi tạo. |
Tên nhánh mặc định của bạn
Theo mặc định, Git sẽ tạo một nhánh có tên là master khi bạn tạo một kho lưu trữ mới với git init.
Từ phiên bản Git 2.28 trở đi, bạn có thể đặt một tên khác cho nhánh ban đầu.
Để đặt main làm tên nhánh mặc định, hãy làm:
$ git config --global init.defaultBranch main
Kiểm tra Cài đặt của Bạn
Nếu bạn muốn kiểm tra cài đặt cấu hình của mình, bạn có thể sử dụng lệnh git config --list để liệt kê tất cả các cài đặt mà Git có thể tìm thấy tại thời điểm đó:
$ git config --list
user.name=John Doe
user.email=johndoe@example.com
color.status=auto
color.branch=auto
color.interactive=auto
color.diff=auto
...
Bạn có thể thấy các khóa nhiều hơn một lần, bởi vì Git đọc cùng một khóa từ các tệp khác nhau ([path]/etc/gitconfig và ~/.gitconfig, chẳng hạn).
Trong trường hợp này, Git sử dụng giá trị cuối cùng cho mỗi khóa duy nhất mà nó nhìn thấy.
Bạn cũng có thể kiểm tra xem Git nghĩ giá trị của một khóa cụ thể là gì bằng cách nhập git config <key>:
$ git config user.name
John Doe
|
Vì Git có thể đọc cùng một giá trị biến cấu hình từ nhiều hơn một tệp, có thể bạn có một giá trị không mong muốn cho một trong những giá trị này và bạn không biết tại sao. Trong những trường hợp như vậy, bạn có thể truy vấn Git về nguồn gốc cho giá trị đó, và nó sẽ cho bạn biết tệp cấu hình nào có tiếng nói cuối cùng trong việc thiết lập giá trị đó:
|
Nhận Trợ giúp
Nếu bạn cần trợ giúp khi sử dụng Git, có ba cách tương đương để nhận trợ giúp trang hướng dẫn toàn diện (manpage) cho bất kỳ lệnh Git nào:
$ git help <verb>
$ git <verb> --help
$ man git-<verb>
Ví dụ, bạn có thể nhận trợ giúp manpage cho lệnh git config bằng cách chạy lệnh này:
$ git help config
Các lệnh này rất hay vì bạn có thể truy cập chúng ở bất cứ đâu, ngay cả khi ngoại tuyến.
Nếu các trang hướng dẫn và cuốn sách này không đủ và bạn cần trợ giúp trực tiếp, bạn có thể thử các kênh #git, #github, hoặc #gitlab trên máy chủ Libera Chat IRC, có thể tìm thấy tại https://libera.chat/.
Các kênh này thường xuyên có hàng trăm người am hiểu về Git và thường sẵn sàng giúp đỡ.
Ngoài ra, nếu bạn không cần trợ giúp manpage đầy đủ, mà chỉ cần xem lại nhanh các tùy chọn có sẵn cho một lệnh Git, bạn có thể yêu cầu đầu ra “help” ngắn gọn hơn với tùy chọn -h, như trong:
$ git add -h
usage: git add [<options>] [--] <pathspec>...
-n, --dry-run dry run
-v, --verbose be verbose
-i, --interactive interactive picking
-p, --patch select hunks interactively
-e, --edit edit current diff and apply
-f, --force allow adding otherwise ignored files
-u, --update update tracked files
--renormalize renormalize EOL of tracked files (implies -u)
-N, --intent-to-add record only the fact that the path will be added later
-A, --all add changes from all tracked and untracked files
--ignore-removal ignore paths removed in the working tree (same as --no-all)
--refresh don't add, only refresh the index
--ignore-errors just skip files which cannot be added because of errors
--ignore-missing check if - even missing - files are ignored in dry run
--sparse allow updating entries outside of the sparse-checkout cone
--chmod (+|-)x override the executable bit of the listed files
--pathspec-from-file <file> read pathspec from file
--pathspec-file-nul with --pathspec-from-file, pathspec elements are separated with NUL character
Tóm tắt
Bạn nên nắm được khái niệm cơ bản về Git và cách nó khác biệt so với các hệ thống quản lý phiên bản tập trung mà bạn có thể đã sử dụng trước đây. Bạn cũng sẽ có một phiên bản Git hoạt động trên hệ thống của mình và được cấu hình với thông tin cá nhân. Bây giờ là lúc học các kiến thức cơ bản về Git.
Các khái niệm cơ bản về Git
Nếu bạn chỉ đọc một chương để bắt đầu với Git, thì đây là chương đó. Chương này trình bày mọi lệnh cơ bản bạn cần để thực hiện phần lớn công việc hàng ngày với Git. Sau chương này, bạn sẽ biết cách cấu hình và khởi tạo một kho, bắt đầu và ngừng theo dõi tệp, cũng như cách đưa các thay đổi vào vùng dàn (stage) và commit chúng. Chúng tôi cũng sẽ hướng dẫn cách thiết lập Git để bỏ qua một số tệp hoặc mẫu tệp, cách hoàn tác lỗi nhanh chóng, cách duyệt lịch sử dự án và xem khác biệt giữa các commit, cũng như cách push và pull tới các kho từ xa.
Lấy một Kho lưu trữ Git
Bạn thường có được một kho lưu trữ Git theo một trong hai cách:
-
Bạn có thể lấy một thư mục cục bộ hiện không được kiểm soát phiên bản và biến nó thành một kho lưu trữ Git, hoặc
-
Bạn có thể sao chép một kho lưu trữ Git hiện có từ nơi khác.
Trong cả hai trường hợp, bạn sẽ có một kho lưu trữ Git trên máy cục bộ của mình, sẵn sàng để làm việc.
Khởi tạo một Kho lưu trữ trong một Thư mục Hiện có
Nếu bạn có một thư mục dự án hiện không được kiểm soát phiên bản và bạn muốn bắt đầu kiểm soát nó với Git, trước tiên bạn cần đi đến thư mục của dự án đó. Nếu bạn chưa bao giờ làm điều này, nó trông hơi khác một chút tùy thuộc vào hệ thống bạn đang chạy:
cho Linux:
$ cd /home/user/my_project
cho macOS:
$ cd /Users/user/my_project
cho Windows:
$ cd C:/Users/user/my_project
và nhập:
$ git init
Điều này tạo một thư mục con mới có tên .git chứa tất cả các tệp kho lưu trữ cần thiết của bạn — một bộ khung kho lưu trữ Git.
Tại thời điểm này, chưa có gì trong dự án của bạn được theo dõi.
Xem [ch10-git-internals] để biết thêm thông tin về chính xác những tệp nào được chứa trong thư mục .git bạn vừa tạo.
Nếu bạn muốn bắt đầu kiểm soát phiên bản các tệp hiện có (trái ngược với một thư mục trống), bạn có thể nên bắt đầu theo dõi các tệp đó và thực hiện một cam kết ban đầu.
Bạn có thể thực hiện điều đó với một vài lệnh git add chỉ định các tệp bạn muốn theo dõi, theo sau là một git commit:
$ git add *.c
$ git add LICENSE
$ git commit -m 'Initial project version'
Chúng ta sẽ xem xét những lệnh này làm gì chỉ trong một phút. Tại thời điểm này, bạn có một kho lưu trữ Git với các tệp được theo dõi và một cam kết ban đầu.
Sao chép một Kho lưu trữ Hiện có
Nếu bạn muốn lấy một bản sao của một kho lưu trữ Git hiện có — ví dụ, một dự án bạn muốn đóng góp — lệnh bạn cần là git clone.
Nếu bạn quen thuộc với các VCS khác như Subversion, bạn sẽ nhận thấy rằng lệnh là "clone" chứ không phải "checkout".
Đây là một sự khác biệt quan trọng — thay vì chỉ nhận được một bản sao làm việc, Git nhận được một bản sao đầy đủ của gần như tất cả dữ liệu mà máy chủ có.
Mọi phiên bản của mọi tệp cho lịch sử của dự án được kéo xuống theo mặc định khi bạn chạy git clone.
Trên thực tế, nếu đĩa máy chủ của bạn bị hỏng, bạn thường có thể sử dụng gần như bất kỳ bản sao nào trên bất kỳ máy khách nào để đặt máy chủ trở lại trạng thái nó đã ở khi nó được sao chép (bạn có thể mất một số hook phía máy chủ và như vậy, nhưng tất cả dữ liệu được tạo phiên bản sẽ ở đó — xem [_getting_git_on_a_server] để biết thêm chi tiết).
Bạn sao chép một kho lưu trữ với git clone <url>.
Ví dụ, nếu bạn muốn sao chép thư viện có thể liên kết Git có tên libgit2, bạn có thể làm như thế này:
$ git clone https://github.com/libgit2/libgit2
Điều đó tạo một thư mục có tên libgit2, khởi tạo một thư mục .git bên trong nó, kéo xuống tất cả dữ liệu cho kho lưu trữ đó và kiểm xuất một bản sao làm việc của phiên bản mới nhất.
Nếu bạn vào thư mục libgit2 mới vừa được tạo, bạn sẽ thấy các tệp dự án ở đó, sẵn sàng để được làm việc hoặc sử dụng.
Nếu bạn muốn sao chép kho lưu trữ vào một thư mục có tên khác với libgit2, bạn có thể chỉ định tên thư mục mới làm đối số bổ sung:
$ git clone https://github.com/libgit2/libgit2 mylibgit
Lệnh đó làm điều tương tự như lệnh trước đó, nhưng thư mục đích được gọi là mylibgit.
Git có một số giao thức truyền khác nhau mà bạn có thể sử dụng.
Ví dụ trước sử dụng giao thức https://, nhưng bạn cũng có thể thấy git:// hoặc user@server:path/to/repo.git, sử dụng giao thức truyền SSH.
[_getting_git_on_a_server] sẽ giới thiệu tất cả các tùy chọn có sẵn mà máy chủ có thể thiết lập để truy cập kho lưu trữ Git của bạn và ưu và nhược điểm của mỗi tùy chọn.
Ghi lại các Thay đổi vào Kho chứa
Đến đây, bạn đã có một kho chứa Git thực thụ trên máy cục bộ của mình, và một bản checkout hay bản sao làm việc của tất cả các file của nó ở trước mặt. Thông thường, bạn sẽ muốn bắt đầu thực hiện các thay đổi và commit các snapshot của những thay đổi đó vào kho chứa của mình mỗi khi dự án đạt đến một trạng thái mà bạn muốn ghi lại.
Hãy nhớ rằng mỗi file trong thư mục làm việc của bạn có thể ở một trong hai trạng thái: được theo dõi (tracked) hoặc không được theo dõi (untracked). Các file được theo dõi là những file đã có trong snapshot cuối cùng, cũng như bất kỳ file mới nào được đưa vào khu vực tổ chức (staged); chúng có thể ở trạng thái chưa bị sửa đổi, đã bị sửa đổi, hoặc đã được tổ chức. Nói tóm lại, các file được theo dõi là những file mà Git biết đến.
Các file không được theo dõi là tất cả những file còn lại — bất kỳ file nào trong thư mục làm việc của bạn mà không có trong snapshot cuối cùng của bạn và không nằm trong khu vực tổ chức của bạn. Khi bạn lần đầu tiên sao chép (clone) một kho chứa, tất cả các file của bạn sẽ ở trạng thái được theo dõi và chưa bị sửa đổi vì Git vừa mới checkout chúng và bạn chưa chỉnh sửa bất cứ thứ gì.
Khi bạn chỉnh sửa các file, Git xem chúng là đã bị sửa đổi, bởi vì bạn đã thay đổi chúng kể từ lần commit cuối cùng của mình. Khi bạn làm việc, bạn chọn lọc đưa các file đã bị sửa đổi này vào khu vực tổ chức và sau đó commit tất cả những thay đổi đã được tổ chức đó, và chu trình lặp lại.
Kiểm tra Trạng thái các File của bạn
Công cụ chính bạn sử dụng để xác định file nào đang ở trạng thái nào là lệnh git status.
Nếu bạn chạy lệnh này ngay sau khi clone, bạn sẽ thấy kết quả tương tự như sau:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean
Điều này có nghĩa là bạn có một thư mục làm việc sạch; nói cách khác, không có file nào được theo dõi của bạn bị sửa đổi.
Git cũng không thấy bất kỳ file không được theo dõi nào, nếu có chúng sẽ được liệt kê ở đây.
Cuối cùng, lệnh này cho bạn biết bạn đang ở trên nhánh nào và thông báo rằng nó không bị phân kỳ so với nhánh cùng tên trên máy chủ.
Hiện tại, nhánh đó luôn là master, là nhánh mặc định; bạn không cần lo lắng về nó ở đây.
Nhánh trong Git sẽ đi sâu vào các nhánh và tham chiếu một cách chi tiết.
|
GitHub đã thay đổi tên nhánh mặc định từ Tuy nhiên, bản thân Git vẫn sử dụng |
Giả sử bạn thêm một file mới vào dự án của mình, một file README đơn giản.
Nếu file này chưa tồn tại trước đó, và bạn chạy git status, bạn sẽ thấy file không được theo dõi của mình như sau:
$ echo 'Dự án của tôi' > README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
README
nothing added to commit but untracked files present (use "git add" to track)
Bạn có thể thấy rằng file README mới của bạn không được theo dõi, bởi vì nó nằm dưới tiêu đề “Untracked files” trong kết quả trạng thái của bạn.
Không được theo dõi về cơ bản có nghĩa là Git thấy một file mà bạn không có trong snapshot trước đó (commit), và nó chưa được tổ chức; Git sẽ không bắt đầu bao gồm nó trong các snapshot commit của bạn cho đến khi bạn nói rõ cho nó làm vậy.
Nó làm điều này để bạn không vô tình bắt đầu bao gồm các file nhị phân được tạo ra hoặc các file khác mà bạn không có ý định bao gồm.
Bạn muốn bắt đầu bao gồm README, vậy hãy bắt đầu theo dõi file này.
Theo dõi File mới
Để bắt đầu theo dõi một file mới, bạn sử dụng lệnh git add.
Để bắt đầu theo dõi file README, bạn có thể chạy:
$ git add README
Nếu bạn chạy lại lệnh status, bạn có thể thấy rằng file README của bạn hiện đã được theo dõi và được tổ chức để commit:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: README
Bạn có thể biết rằng nó đã được tổ chức vì nó nằm dưới tiêu đề “Changes to be committed”.
Nếu bạn commit tại thời điểm này, phiên bản của file tại thời điểm bạn chạy git add là phiên bản sẽ có trong snapshot lịch sử tiếp theo.
Bạn có thể nhớ lại rằng khi bạn chạy git init trước đó, bạn sau đó đã chạy git add <files> — đó là để bắt đầu theo dõi các file trong thư mục của bạn.
Lệnh git add nhận một tên đường dẫn cho một file hoặc một thư mục; nếu đó là một thư mục, lệnh sẽ thêm tất cả các file trong thư mục đó một cách đệ quy.
Tổ chức các File đã bị sửa đổi
Hãy thay đổi một file đã được theo dõi.
Nếu bạn thay đổi một file đã được theo dõi trước đó có tên là CONTRIBUTING.md và sau đó chạy lại lệnh git status, bạn sẽ nhận được một kết quả trông như thế này:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
File CONTRIBUTING.md xuất hiện dưới một phần có tên “Changes not staged for commit” — điều này có nghĩa là một file đang được theo dõi đã bị sửa đổi trong thư mục làm việc nhưng chưa được tổ chức.
Để tổ chức nó, bạn chạy lệnh git add.
git add là một lệnh đa năng — bạn sử dụng nó để bắt đầu theo dõi các file mới, để tổ chức các file, và để làm những việc khác như đánh dấu các file bị xung đột khi gộp (merge-conflicted) là đã được giải quyết.
Có thể hữu ích khi nghĩ về nó nhiều hơn như là “thêm chính xác nội dung này vào commit tiếp theo” hơn là “thêm file này vào dự án”.
Hãy chạy git add ngay bây giờ để tổ chức file CONTRIBUTING.md, và sau đó chạy lại git status:
$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: README
modified: CONTRIBUTING.md
Cả hai file đều đã được tổ chức và sẽ được đưa vào commit tiếp theo của bạn.
Tại thời điểm này, giả sử bạn nhớ ra một thay đổi nhỏ mà bạn muốn thực hiện trong CONTRIBUTING.md trước khi commit.
Bạn mở lại nó và thực hiện thay đổi đó, và bạn đã sẵn sàng để commit.
Tuy nhiên, hãy chạy git status một lần nữa:
$ vim CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README
modified: CONTRIBUTING.md
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
Cái quái gì vậy?
Bây giờ CONTRIBUTING.md được liệt kê là vừa được tổ chức và chưa được tổ chức.
Làm thế nào điều đó có thể xảy ra?
Hóa ra Git tổ chức một file chính xác như nó có khi bạn chạy lệnh git add.
Nếu bạn commit bây giờ, phiên bản của CONTRIBUTING.md như khi bạn chạy lệnh git add lần cuối là cách nó sẽ được đưa vào commit, chứ không phải phiên bản của file như nó trông trong thư mục làm việc của bạn khi bạn chạy git commit.
Nếu bạn sửa đổi một file sau khi bạn chạy git add, bạn phải chạy git add một lần nữa để tổ chức phiên bản mới nhất của file:
$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README
modified: CONTRIBUTING.md
Trạng thái Ngắn gọn
Trong khi kết quả git status khá toàn diện, nó cũng khá dài dòng.
Git cũng có một cờ trạng thái ngắn gọn để bạn có thể xem các thay đổi của mình một cách gọn gàng hơn.
Nếu bạn chạy git status -s hoặc git status --short, bạn sẽ nhận được một kết quả đơn giản hơn nhiều từ lệnh:
$ git status -s
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt
Các file mới không được theo dõi có một dấu ?? bên cạnh, các file mới đã được thêm vào khu vực tổ chức có một A, các file bị sửa đổi có một M, v.v.
Có hai cột trong kết quả — cột bên trái cho biết trạng thái của khu vực tổ chức và cột bên phải cho biết trạng thái của cây làm việc.
Ví dụ trong kết quả đó, file README bị sửa đổi trong thư mục làm việc nhưng chưa được tổ chức, trong khi file lib/simplegit.rb bị sửa đổi và đã được tổ chức.
File Rakefile đã bị sửa đổi, được tổ chức và sau đó bị sửa đổi một lần nữa, vì vậy có những thay đổi cho nó vừa được tổ chức vừa chưa được tổ chức.
Bỏ qua các File
Thường thì, bạn sẽ có một loại file mà bạn không muốn Git tự động thêm hoặc thậm chí hiển thị cho bạn là đang không được theo dõi.
Đây thường là các file được tạo tự động như file log hoặc các file được tạo ra bởi hệ thống build của bạn.
Trong những trường hợp như vậy, bạn có thể tạo một file liệt kê các mẫu để khớp với chúng có tên là .gitignore.
Đây là một ví dụ về file .gitignore:
$ cat .gitignore
*.a
*~
Dòng đầu tiên yêu cầu Git bỏ qua bất kỳ file nào kết thúc bằng ".a" — các file đối tượng và file lưu trữ có thể là sản phẩm của việc build mã của bạn.
Dòng thứ hai yêu cầu Git bỏ qua tất cả các file có tên kết thúc bằng dấu ngã (~), được nhiều trình soạn thảo văn bản như Emacs sử dụng để đánh dấu các file tạm thời.
Bạn cũng có thể bao gồm một thư mục log, tmp, hoặc pid; tài liệu được tạo tự động; v.v.
Việc thiết lập một file .gitignore cho kho chứa mới của bạn trước khi bắt đầu thường là một ý kiến hay để bạn không vô tình commit các file mà bạn thực sự không muốn có trong kho chứa Git của mình.
Các quy tắc cho các mẫu bạn có thể đặt trong file .gitignore như sau:
-
Các dòng trống hoặc các dòng bắt đầu bằng
#sẽ bị bỏ qua. -
Các mẫu glob tiêu chuẩn hoạt động và sẽ được áp dụng đệ quy trong toàn bộ cây làm việc.
-
Bạn có thể bắt đầu các mẫu bằng một dấu gạch chéo (
/) để tránh đệ quy. -
Bạn có thể kết thúc các mẫu bằng một dấu gạch chéo (
/) để chỉ định một thư mục. -
Bạn có thể phủ định một mẫu bằng cách bắt đầu nó bằng một dấu chấm than (
!).
Các mẫu glob giống như các biểu thức chính quy được đơn giản hóa mà các shell sử dụng.
Một dấu hoa thị () khớp với không hoặc nhiều ký tự; [abc] khớp với bất kỳ ký tự nào bên trong dấu ngoặc (trong trường hợp này là a, b, hoặc c); một dấu chấm hỏi (?) khớp với một ký tự duy nhất; và dấu ngoặc vuông chứa các ký tự được phân tách bằng dấu gạch ngang ([0-9]) khớp với bất kỳ ký tự nào giữa chúng (trong trường hợp này là từ 0 đến 9).
Bạn cũng có thể sử dụng hai dấu hoa thị để khớp với các thư mục lồng nhau; a/*/z sẽ khớp với a/z, a/b/z, a/b/c/z, v.v.
Đây là một ví dụ khác về file .gitignore:
# bỏ qua tất cả các file .a
*.a
# nhưng theo dõi lib.a, mặc dù bạn đang bỏ qua các file .a ở trên
!lib.a
# chỉ bỏ qua file TODO trong thư mục hiện tại, không phải subdir/TODO
/TODO
# bỏ qua tất cả các file trong bất kỳ thư mục nào có tên là build
build/
# bỏ qua doc/notes.txt, nhưng không phải doc/server/arch.txt
doc/*.txt
# bỏ qua tất cả các file .pdf trong thư mục doc/ và bất kỳ thư mục con nào của nó
doc/**/*.pdf
|
GitHub duy trì một danh sách khá toàn diện các ví dụ file |
|
Trong trường hợp đơn giản, một kho chứa có thể có một file Việc đi vào chi tiết về nhiều file |
Xem các Thay đổi đã được Tổ chức và Chưa được Tổ chức của bạn
Nếu lệnh git status quá mơ hồ đối với bạn — bạn muốn biết chính xác những gì bạn đã thay đổi, không chỉ là những file nào đã được thay đổi — bạn có thể sử dụng lệnh git diff.
Chúng ta sẽ tìm hiểu chi tiết hơn về git diff sau này, nhưng bạn có thể sẽ sử dụng nó thường xuyên nhất để trả lời hai câu hỏi sau: Bạn đã thay đổi những gì nhưng chưa được tổ chức?
Và bạn đã tổ chức những gì mà bạn sắp commit?
Mặc dù git status trả lời những câu hỏi đó một cách rất chung chung bằng cách liệt kê tên file, git diff cho bạn thấy các dòng chính xác đã được thêm và xóa — bản vá, có thể nói như vậy.
Giả sử bạn chỉnh sửa và tổ chức file README một lần nữa và sau đó chỉnh sửa file CONTRIBUTING.md mà không tổ chức nó.
Nếu bạn chạy lệnh git status, bạn sẽ một lần nữa thấy một cái gì đó như thế này:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
Để xem những gì bạn đã thay đổi nhưng chưa được tổ chức, hãy gõ git diff mà không có đối số nào khác:
$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
Please include a nice description of your changes when you submit your PR;
if we have to read the whole diff to figure out why you're contributing
in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.
If you are starting to work on a particular area, feel free to submit a PR
that highlights your work in progress (and note in the PR title that it's
Lệnh đó so sánh những gì có trong thư mục làm việc của bạn với những gì có trong khu vực tổ chức của bạn. Kết quả cho bạn biết những thay đổi bạn đã thực hiện mà bạn chưa tổ chức.
Nếu bạn muốn xem những gì bạn đã tổ chức sẽ được đưa vào commit tiếp theo, bạn có thể sử dụng git diff --staged.
Lệnh này so sánh các thay đổi đã được tổ chức của bạn với commit cuối cùng của bạn:
$ git diff --staged
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+Dự án của tôi
Điều quan trọng cần lưu ý là git diff tự nó không hiển thị tất cả các thay đổi được thực hiện kể từ commit cuối cùng của bạn — chỉ những thay đổi vẫn chưa được tổ chức.
Nếu bạn đã tổ chức tất cả các thay đổi của mình, git diff sẽ không có kết quả đầu ra.
Đối với một ví dụ khác, nếu bạn tổ chức file CONTRIBUTING.md và sau đó chỉnh sửa nó, bạn có thể sử dụng git diff để xem các thay đổi trong file đã được tổ chức và các thay đổi chưa được tổ chức.
Nếu môi trường của chúng ta trông như thế này:
$ git add CONTRIBUTING.md
$ echo '# dòng test' >> CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: CONTRIBUTING.md
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
Bây giờ bạn có thể sử dụng git diff để xem những gì vẫn chưa được tổ chức:
$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 643e24f..87f08c8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -119,3 +119,4 @@ at the
## Starter Projects
See our [projects list](https://github.com/libgit2/libgit2/blob/development/PROJECTS.md).
+# dòng test
và git diff --cached để xem những gì bạn đã tổ chức cho đến nay (--staged và --cached là từ đồng nghĩa):
$ git diff --cached
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
Please include a nice description of your changes when you submit your PR;
if we have to read the whole diff to figure out why you're contributing
in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.
If you are starting to work on a particular area, feel free to submit a PR
that highlights your work in progress (and note in the PR title that it's
|
Git Diff trong một Công cụ Bên ngoài
Chúng ta sẽ tiếp tục sử dụng lệnh |
Commit các Thay đổi của bạn
Bây giờ khu vực tổ chức của bạn đã được thiết lập theo cách bạn muốn, bạn có thể commit các thay đổi của mình.
Hãy nhớ rằng bất cứ thứ gì vẫn chưa được tổ chức — bất kỳ file nào bạn đã tạo hoặc sửa đổi mà bạn chưa chạy git add kể từ khi bạn chỉnh sửa chúng — sẽ không được đưa vào commit này.
Chúng sẽ vẫn là các file đã bị sửa đổi trên đĩa của bạn.
Trong trường hợp này, giả sử rằng lần cuối cùng bạn chạy git status, bạn thấy rằng mọi thứ đã được tổ chức, vì vậy bạn đã sẵn sàng để commit các thay đổi của mình.
Cách đơn giản nhất để commit là gõ git commit:
$ git commit
Làm như vậy sẽ khởi chạy trình soạn thảo mà bạn chọn.
|
Điều này được thiết lập bởi biến môi trường |
Trình soạn thảo hiển thị văn bản sau (ví dụ này là màn hình Vim):
# Vui lòng nhập thông điệp commit cho các thay đổi của bạn. Các dòng bắt đầu
# bằng '#' sẽ bị bỏ qua, và một thông điệp trống sẽ hủy bỏ commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
#\tnew file: README
#\tmodified: CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C
Bạn có thể thấy rằng thông điệp commit mặc định chứa kết quả mới nhất của lệnh git status được chú thích và một dòng trống ở trên cùng.
Bạn có thể xóa các chú thích này và gõ thông điệp commit của mình, hoặc bạn có thể để chúng ở đó để giúp bạn nhớ những gì bạn đang commit.
|
Để có một lời nhắc nhở rõ ràng hơn về những gì bạn đã sửa đổi, bạn có thể truyền tùy chọn |
Khi bạn thoát khỏi trình soạn thảo, Git sẽ tạo commit của bạn với thông điệp commit đó (với các chú thích và diff đã được loại bỏ).
Ngoài ra, bạn có thể gõ thông điệp commit của mình cùng với lệnh commit bằng cách chỉ định nó sau một cờ -m, như thế này:
$ git commit -m "Story 182: fix benchmarks for speed"
[master 463dc4f] Story 182: fix benchmarks for speed
2 files changed, 2 insertions(+)
create mode 100644 README
Bây giờ bạn đã tạo commit đầu tiên của mình!
Bạn có thể thấy rằng commit đã cho bạn một số kết quả về chính nó: bạn đã commit vào nhánh nào (master), commit có checksum SHA-1 nào (463dc4f), bao nhiêu file đã được thay đổi, và thống kê về số dòng đã được thêm và xóa trong commit.
Hãy nhớ rằng commit ghi lại snapshot mà bạn đã thiết lập trong khu vực tổ chức của mình. Bất cứ thứ gì bạn không tổ chức vẫn còn ở đó bị sửa đổi; bạn có thể thực hiện một commit khác để thêm nó vào lịch sử của mình. Mỗi lần bạn thực hiện một commit, bạn đang ghi lại một snapshot của dự án của mình mà bạn có thể hoàn nguyên về hoặc so sánh với sau này.
Bỏ qua Khu vực Tổ chức
Mặc dù nó có thể cực kỳ hữu ích để tạo các commit chính xác như bạn muốn, khu vực tổ chức đôi khi hơi phức tạp hơn bạn cần trong quy trình làm việc của mình.
Nếu bạn muốn bỏ qua khu vực tổ chức, Git cung cấp một phím tắt đơn giản.
Thêm tùy chọn -a vào lệnh git commit làm cho Git tự động tổ chức mọi file đã được theo dõi trước khi thực hiện commit, cho phép bạn bỏ qua phần git add:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m 'Thêm benchmark mới'
[master 83e38c7] Thêm benchmark mới
1 file changed, 5 insertions(+), 0 deletions(-)
Lưu ý cách bạn không phải chạy git add trên file CONTRIBUTING.md trong trường hợp này trước khi bạn commit.
Đó là bởi vì cờ -a bao gồm tất cả các file đã thay đổi.
Điều này tiện lợi, nhưng hãy cẩn thận; đôi khi cờ này sẽ khiến bạn bao gồm các thay đổi không mong muốn.
Xóa các File
Để xóa một file khỏi Git, bạn phải xóa nó khỏi các file được theo dõi của mình (chính xác hơn là xóa nó khỏi khu vực tổ chức của bạn) và sau đó commit.
Lệnh git rm thực hiện điều đó, và cũng xóa file khỏi thư mục làm việc của bạn để bạn không thấy nó là một file không được theo dõi vào lần sau.
Nếu bạn chỉ cần xóa file khỏi thư mục làm việc của mình, nó sẽ xuất hiện dưới khu vực “Changes not staged for commit” (nghĩa là, chưa được tổ chức) của kết quả git status của bạn:
$ rm PROJECTS.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: PROJECTS.md
no changes added to commit (use "git add" and/or "git commit -a")
Sau đó, nếu bạn chạy git rm, nó sẽ tổ chức việc xóa file:
$ git rm PROJECTS.md
rm 'PROJECTS.md'
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
deleted: PROJECTS.md
Lần commit tiếp theo, file sẽ biến mất và không còn được theo dõi nữa.
Nếu bạn đã sửa đổi file hoặc đã thêm nó vào khu vực tổ chức, bạn phải buộc xóa bằng tùy chọn -f.
Đây là một tính năng an toàn để ngăn chặn việc vô tình xóa dữ liệu chưa được ghi vào snapshot và không thể phục hồi từ Git.
Một điều hữu ích khác bạn có thể muốn làm là giữ file trong cây làm việc của bạn nhưng xóa nó khỏi khu vực tổ chức của bạn.
Nói cách khác, bạn có thể muốn giữ file trên ổ cứng của mình nhưng không muốn Git theo dõi nó nữa.
Điều này đặc biệt hữu ích nếu bạn quên thêm một cái gì đó vào file .gitignore của mình và vô tình tổ chức nó, như một file log lớn hoặc một loạt các file đã biên dịch .a.
Để làm điều này, hãy sử dụng tùy chọn --cached:
$ git rm --cached README
Bạn có thể truyền các file, thư mục, và các mẫu glob file cho lệnh git rm.
Điều đó có nghĩa là bạn có thể làm những việc như sau:
$ git rm log/\*.log
Lưu ý dấu gạch chéo ngược (\) ở phía trước *.
Điều này là cần thiết vì Git thực hiện việc mở rộng tên file của riêng nó ngoài việc mở rộng tên file của shell của bạn.
Lệnh này xóa tất cả các file có phần mở rộng .log trong thư mục log/.
Hoặc, bạn có thể làm một cái gì đó như thế này:
$ git rm \*~
Lệnh này xóa tất cả các file có tên kết thúc bằng ~.
Di chuyển các File
Không giống như nhiều VCS khác, Git không theo dõi chuyển động file một cách rõ ràng. Nếu bạn đổi tên một file trong Git, không có siêu dữ liệu nào được lưu trữ trong Git cho biết bạn đã đổi tên file. Tuy nhiên, Git khá thông minh trong việc tìm ra điều đó sau đó — chúng ta sẽ xử lý việc phát hiện chuyển động file một chút sau.
Do đó, hơi khó hiểu khi Git có lệnh mv.
Nếu bạn muốn đổi tên một file trong Git, bạn có thể chạy một cái gì đó như:
$ git mv file_from file_to
và nó hoạt động tốt. Thực tế, nếu bạn chạy một cái gì đó như thế này và xem trạng thái, bạn sẽ thấy rằng Git coi đó là một file đã được đổi tên:
$ git mv README.md README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
Tuy nhiên, điều này tương đương với việc chạy một cái gì đó như thế này:
$ mv README.md README
$ git rm README.md
$ git add README
Git tự động tìm ra đó là một lần đổi tên, vì vậy không quan trọng bạn đổi tên một file theo cách đó hay bằng lệnh mv.
Sự khác biệt thực sự duy nhất là git mv là một lệnh thay vì ba — đó là một hàm tiện lợi.
Quan trọng hơn, bạn có thể sử dụng bất kỳ công cụ nào bạn thích để đổi tên một file, và giải quyết add/rm sau, trước khi bạn commit.
Xem Lịch sử Cam kết
Sau khi bạn đã tạo một vài cam kết, hoặc nếu bạn đã sao chép một kho lưu trữ với lịch sử cam kết hiện có, bạn có thể sẽ muốn nhìn lại để xem những gì đã xảy ra.
Công cụ cơ bản và mạnh mẽ nhất để làm điều này là lệnh git log.
Các ví dụ này sử dụng một dự án rất đơn giản gọi là “simplegit”. Để lấy dự án, chạy:
$ git clone https://github.com/schacon/simplegit-progit
Khi bạn chạy git log trong dự án này, bạn sẽ nhận được đầu ra trông giống như thế này:
$ git log
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
Remove unnecessary test
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 10:31:28 2008 -0700
Initial commit
Theo mặc định, không có đối số, git log liệt kê các cam kết được thực hiện trong kho lưu trữ đó theo thứ tự thời gian ngược lại; nghĩa là, các cam kết gần đây nhất hiển thị đầu tiên.
Như bạn có thể thấy, lệnh này liệt kê mỗi cam kết với tổng kiểm tra SHA-1 của nó, tên và email của tác giả, ngày viết và thông điệp cam kết.
Một số lượng lớn và đa dạng các tùy chọn cho lệnh git log có sẵn để hiển thị cho bạn chính xác những gì bạn đang tìm kiếm.
Ở đây, chúng tôi sẽ chỉ cho bạn một số tùy chọn phổ biến nhất.
Một trong những tùy chọn hữu ích hơn là -p hoặc --patch, hiển thị sự khác biệt (đầu ra patch) được giới thiệu trong mỗi cam kết.
Bạn cũng có thể giới hạn số lượng mục nhật ký được hiển thị, chẳng hạn như sử dụng -2 để chỉ hiển thị hai mục cuối cùng.
$ git log -p -2
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
diff --git a/Rakefile b/Rakefile
index a874b73..8f94139 100644
--- a/Rakefile
+++ b/Rakefile
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = "simplegit"
- s.version = "0.1.0"
+ s.version = "0.1.1"
s.author = "Scott Chacon"
s.email = "schacon@gee-mail.com"
s.summary = "A simple gem for using Git in Ruby code."
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
Remove unnecessary test
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index a0a60ae..47c6340 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -18,8 +18,3 @@ class SimpleGit
end
end
-
-if $0 == __FILE__
- git = SimpleGit.new
- puts git.show
-end
Tùy chọn này hiển thị cùng một thông tin nhưng với một diff trực tiếp theo sau mỗi mục.
Điều này rất hữu ích cho việc xem xét mã hoặc để nhanh chóng duyệt những gì đã xảy ra trong một loạt các cam kết mà một cộng tác viên đã thêm vào.
Bạn cũng có thể sử dụng một loạt các tùy chọn tóm tắt với git log.
Ví dụ, nếu bạn muốn xem một số thống kê tóm tắt cho mỗi cam kết, bạn có thể sử dụng tùy chọn --stat:
$ git log --stat
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
Rakefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
Remove unnecessary test
lib/simplegit.rb | 5 -----
1 file changed, 5 deletions(-)
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 10:31:28 2008 -0700
Initial commit
README | 6 ++++++
Rakefile | 23 +++++++++++++++++++++++
lib/simplegit.rb | 25 +++++++++++++++++++++++++
3 files changed, 54 insertions(+)
Như bạn có thể thấy, tùy chọn --stat in bên dưới mỗi mục cam kết một danh sách các tệp đã sửa đổi, bao nhiêu tệp đã thay đổi và bao nhiêu dòng trong các tệp đó đã được thêm và xóa.
Nó cũng đặt một bản tóm tắt thông tin ở cuối.
Một tùy chọn thực sự hữu ích khác là --pretty.
Tùy chọn này thay đổi đầu ra nhật ký sang các định dạng khác ngoài mặc định.
Một vài giá trị tùy chọn dựng sẵn có sẵn cho bạn sử dụng.
Giá trị oneline cho tùy chọn này in mỗi cam kết trên một dòng duy nhất, rất hữu ích nếu bạn đang xem xét rất nhiều cam kết.
Ngoài ra, các giá trị short, full và fuller hiển thị đầu ra theo định dạng gần giống nhau nhưng với ít hoặc nhiều thông tin hơn, tương ứng:
$ git log --pretty=oneline
ca82a6dff817ec66f44342007202690a93763949 Change version number
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Remove unnecessary test
a11bef06a3f659402fe7563abf99ad00de2209e6 Initial commit
Giá trị tùy chọn thú vị nhất là format, cho phép bạn chỉ định định dạng đầu ra nhật ký của riêng mình.
Điều này đặc biệt hữu ích khi bạn đang tạo đầu ra cho máy phân tích cú pháp — bởi vì bạn chỉ định định dạng một cách rõ ràng, bạn biết nó sẽ không thay đổi với các bản cập nhật cho Git:
$ git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Scott Chacon, 6 years ago : Change version number
085bb3b - Scott Chacon, 6 years ago : Remove unnecessary test
a11bef0 - Scott Chacon, 6 years ago : Initial commit
Các chỉ định hữu ích cho git log --pretty=format liệt kê một số chỉ định hữu ích hơn mà format nhận.
| Chỉ định | Mô tả Đầu ra |
|---|---|
|
Hash cam kết |
|
Hash cam kết viết tắt |
|
Hash cây |
|
Hash cây viết tắt |
|
Hash cha |
|
Hash cha viết tắt |
|
Tên tác giả |
|
Email tác giả |
|
Ngày tác giả (định dạng tuân theo |
|
Ngày tác giả, tương đối |
|
Tên người cam kết |
|
Email người cam kết |
|
Ngày người cam kết |
|
Ngày người cam kết, tương đối |
|
Chủ đề |
Bạn có thể tự hỏi sự khác biệt giữa author (tác giả) và committer (người cam kết) là gì. Tác giả là người ban đầu viết công việc, trong khi người cam kết là người cuối cùng áp dụng công việc. Vì vậy, nếu bạn gửi một bản vá cho một dự án và một trong những thành viên cốt lõi áp dụng bản vá, cả hai bạn đều nhận được tín dụng — bạn là tác giả và thành viên cốt lõi là người cam kết. Chúng ta sẽ đề cập đến sự phân biệt này một chút nữa trong Git phân tán.
Các giá trị tùy chọn oneline và format đặc biệt hữu ích với một tùy chọn log khác gọi là --graph.
Tùy chọn này thêm một biểu đồ ASCII nhỏ đẹp hiển thị lịch sử nhánh và hợp nhất của bạn:
$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 Ignore errors from SIGCHLD on trap
* 5e3ee11 Merge branch 'master' of https://github.com/dustin/grit.git
|\
| * 420eac9 Add method for getting the current branch
* | 30e367c Timeout code and tests
* | 5a09431 Add timeout protection to grit
* | e1193f8 Support for heads with slashes in them
|/
* d6016bc Require time for xmlschema
* 11d191e Merge branch 'defunkt' into local
Loại đầu ra này sẽ trở nên thú vị hơn khi chúng ta đi qua việc phân nhánh và hợp nhất trong chương tiếp theo.
Đó chỉ là một số tùy chọn định dạng đầu ra đơn giản cho git log — còn rất nhiều tùy chọn khác.
Các tùy chọn phổ biến cho git log liệt kê các tùy chọn chúng ta đã đề cập cho đến nay, cũng như một số tùy chọn định dạng phổ biến khác có thể hữu ích, cùng với cách chúng thay đổi đầu ra của lệnh log.
| Tùy chọn | Mô tả |
|---|---|
|
Hiển thị bản vá được giới thiệu với mỗi cam kết. |
|
Hiển thị thống kê cho các tệp được sửa đổi trong mỗi cam kết. |
|
Chỉ hiển thị dòng thay đổi/chèn/xóa từ lệnh |
|
Hiển thị danh sách các tệp được sửa đổi sau thông tin cam kết. |
|
Hiển thị danh sách các tệp bị ảnh hưởng với thông tin thêm/sửa đổi/xóa cũng như vậy. |
|
Chỉ hiển thị vài ký tự đầu tiên của tổng kiểm tra SHA-1 thay vì tất cả 40. |
|
Hiển thị ngày ở định dạng tương đối (ví dụ: “2 weeks ago”) thay vì sử dụng định dạng ngày đầy đủ. |
|
Hiển thị biểu đồ ASCII của lịch sử nhánh và hợp nhất bên cạnh đầu ra nhật ký. |
|
Hiển thị các cam kết ở định dạng thay thế. Các giá trị tùy chọn bao gồm |
|
Viết tắt cho |
Giới hạn Đầu ra Nhật ký
Ngoài các tùy chọn định dạng đầu ra, git log nhận một số tùy chọn giới hạn hữu ích; nghĩa là, các tùy chọn cho phép bạn chỉ hiển thị một tập hợp con các cam kết.
Bạn đã thấy một tùy chọn như vậy rồi — tùy chọn -2, hiển thị chỉ hai cam kết cuối cùng.
Trên thực tế, bạn có thể thực hiện -<n>, trong đó n là bất kỳ số nguyên nào để hiển thị n cam kết cuối cùng.
Trong thực tế, bạn không có khả năng sử dụng điều đó thường xuyên, bởi vì Git theo mặc định chuyển tất cả đầu ra qua một máy nhắn tin để bạn chỉ thấy một trang đầu ra nhật ký tại một thời điểm.
Tuy nhiên, các tùy chọn giới hạn thời gian như --since và --until rất hữu ích.
Ví dụ, lệnh này lấy danh sách các cam kết được thực hiện trong hai tuần qua:
$ git log --since=2.weeks
Lệnh này hoạt động với rất nhiều định dạng — bạn có thể chỉ định một ngày cụ thể như "2008-01-15", hoặc một ngày tương đối như "2 years 1 day 3 minutes ago".
Bạn cũng có thể lọc danh sách các cam kết khớp với một số tiêu chí tìm kiếm.
Tùy chọn --author cho phép bạn lọc theo một tác giả cụ thể và tùy chọn --grep cho phép bạn tìm kiếm các từ khóa trong các thông điệp cam kết.
|
Bạn có thể chỉ định nhiều hơn một phiên bản của cả tiêu chí tìm kiếm |
Một bộ lọc thực sự hữu ích khác là tùy chọn -S (thường được gọi là tùy chọn “pickaxe” của Git), lấy một chuỗi và chỉ hiển thị những cam kết đã thay đổi số lần xuất hiện của chuỗi đó.
Ví dụ, nếu bạn muốn tìm cam kết cuối cùng đã thêm hoặc xóa một tham chiếu đến một hàm cụ thể, bạn có thể gọi:
$ git log -S function_name
Tùy chọn thực sự hữu ích cuối cùng để chuyển đến git log dưới dạng bộ lọc là một đường dẫn.
Nếu bạn chỉ định một thư mục hoặc tên tệp, bạn có thể giới hạn đầu ra nhật ký cho các cam kết đã giới thiệu một thay đổi đối với các tệp đó.
Đây luôn là tùy chọn cuối cùng và thường được đặt trước bởi dấu gạch ngang kép (--) để tách các đường dẫn khỏi các tùy chọn:
$ git log -- path/to/file
Trong Các tùy chọn để giới hạn đầu ra của git log chúng tôi sẽ liệt kê các tùy chọn này và một vài tùy chọn phổ biến khác để bạn tham khảo.
| Tùy chọn | Mô tả |
|---|---|
|
Chỉ hiển thị n cam kết cuối cùng. |
|
Giới hạn các cam kết cho những cam kết được thực hiện sau ngày chỉ định. |
|
Giới hạn các cam kết cho những cam kết được thực hiện trước ngày chỉ định. |
|
Chỉ hiển thị các cam kết trong đó mục tác giả khớp với chuỗi chỉ định. |
|
Chỉ hiển thị các cam kết trong đó mục người cam kết khớp với chuỗi chỉ định. |
|
Chỉ hiển thị các cam kết có thông điệp cam kết chứa chuỗi. |
|
Chỉ hiển thị các cam kết thêm hoặc xóa mã khớp với chuỗi. |
Ví dụ, nếu bạn muốn xem những cam kết nào sửa đổi các tệp kiểm tra trong lịch sử mã nguồn Git được cam kết bởi Junio Hamano trong tháng 10 năm 2008 và không phải là các cam kết hợp nhất, bạn có thể chạy một cái gì đó như thế này:
$ git log --pretty="%h - %s" --author='Junio C Hamano' --since="2008-10-01" \
--before="2008-11-01" --no-merges -- t/
5610e3b - Fix testcase failure when extended attributes are in use
acd3b9e - Enhance hold_lock_file_for_{update,append}() API
f563754 - demonstrate breakage of detached checkout with symbolic link HEAD
d1a43f2 - reset --hard/read-tree --reset -u: remove unmerged new paths
51a94af - Fix "checkout --track -b newbranch" on detached HEAD
b0ad11e - pull: allow "git pull origin $something:$current_branch" into an unborn branch
Trong số gần 40.000 cam kết trong lịch sử mã nguồn Git, lệnh này hiển thị 6 cam kết khớp với các tiêu chí đó.
|
Ngăn chặn việc hiển thị các cam kết hợp nhất
Tùy thuộc vào quy trình làm việc được sử dụng trong kho lưu trữ của bạn, có thể một tỷ lệ lớn các cam kết trong lịch sử nhật ký của bạn chỉ là các cam kết hợp nhất, thường không cung cấp nhiều thông tin.
Để ngăn chặn việc hiển thị các cam kết hợp nhất làm lộn xộn lịch sử nhật ký của bạn, chỉ cần thêm tùy chọn |
Hoàn tác Mọi thứ
Ở bất kỳ giai đoạn nào, bạn có thể muốn hoàn tác một cái gì đó. Ở đây, chúng tôi sẽ xem xét một vài công cụ cơ bản để hoàn tác các thay đổi mà bạn đã thực hiện. Hãy cẩn thận, bởi vì bạn không thể luôn luôn hoàn tác một số trong những lần hoàn tác này. Đây là một trong số ít các khu vực trong Git mà bạn có thể mất một số công việc nếu bạn làm sai.
Một trong những lần hoàn tác phổ biến diễn ra khi bạn cam kết quá sớm và có thể quên thêm một số tệp, hoặc bạn làm hỏng thông điệp cam kết của mình.
Nếu bạn muốn làm lại cam kết đó, hãy thực hiện các thay đổi bổ sung mà bạn đã quên, tổ chức chúng và cam kết lại bằng cách sử dụng tùy chọn --amend:
$ git commit --amend
Lệnh này lấy khu vực tổ chức của bạn và sử dụng nó cho cam kết. Nếu bạn không thực hiện thay đổi nào kể từ lần cam kết cuối cùng của mình (ví dụ: bạn chạy lệnh này ngay sau lần cam kết trước đó của mình), thì ảnh chụp nhanh của bạn sẽ trông giống hệt nhau và tất cả những gì bạn sẽ thay đổi là thông điệp cam kết của bạn.
Trình soạn thảo thông điệp cam kết tương tự sẽ kích hoạt, nhưng nó đã chứa thông điệp của cam kết trước đó của bạn. Bạn có thể chỉnh sửa thông điệp giống như mọi khi, nhưng nó ghi đè lên cam kết trước đó của bạn.
Ví dụ, nếu bạn cam kết và sau đó nhận ra bạn quên tổ chức các thay đổi trong một tệp bạn muốn thêm vào cam kết này, bạn có thể làm một cái gì đó như thế này:
$ git commit -m 'Initial commit'
$ git add forgotten_file
$ git commit --amend
Bạn kết thúc với một cam kết duy nhất — cam kết thứ hai thay thế kết quả của cam kết đầu tiên.
|
Điều quan trọng là phải hiểu rằng khi bạn sửa đổi cam kết cuối cùng của mình, bạn không sửa nó nhiều bằng việc thay thế nó hoàn toàn bằng một cam kết mới, được cải thiện đẩy cam kết cũ ra khỏi đường và đặt cam kết mới vào vị trí của nó. Về hiệu quả, nó giống như cam kết trước đó chưa bao giờ xảy ra và nó sẽ không hiển thị trong lịch sử kho lưu trữ của bạn. Giá trị rõ ràng của việc sửa đổi các cam kết là thực hiện các cải tiến nhỏ cho cam kết cuối cùng của bạn, mà không làm lộn xộn lịch sử kho lưu trữ của bạn với các thông điệp cam kết có dạng, “Oops, forgot to add a file” hoặc “Darn, fixing a typo in last commit”. |
|
Chỉ sửa đổi các cam kết vẫn còn cục bộ và chưa được đẩy đi đâu đó. Sửa đổi các cam kết đã đẩy trước đó và buộc đẩy nhánh sẽ gây ra vấn đề cho các cộng tác viên của bạn. Để biết thêm về những gì xảy ra khi bạn làm điều này và cách khôi phục nếu bạn là người nhận, hãy đọc Sự Nguy hiểm của Rebasing. |
Hủy tổ chức một Tệp đã Tổ chức
Hai phần tiếp theo minh họa cách làm việc với khu vực tổ chức và các thay đổi thư mục làm việc của bạn.
Phần hay là lệnh bạn sử dụng để xác định trạng thái của hai khu vực đó cũng nhắc nhở bạn cách hoàn tác các thay đổi đối với chúng.
Ví dụ: giả sử bạn đã thay đổi hai tệp và muốn cam kết chúng dưới dạng hai thay đổi riêng biệt, nhưng bạn vô tình nhập git add * và tổ chức cả hai.
Làm thế nào bạn có thể hủy tổ chức một trong hai?
Lệnh git status nhắc nhở bạn:
$ git add *
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
modified: CONTRIBUTING.md
Ngay bên dưới văn bản “Changes to be committed”, nó nói sử dụng git reset HEAD <file>… để hủy tổ chức.
Vì vậy, hãy sử dụng lời khuyên đó để hủy tổ chức tệp CONTRIBUTING.md:
$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
Lệnh này hơi lạ, nhưng nó hoạt động.
Tệp CONTRIBUTING.md được sửa đổi nhưng một lần nữa không được tổ chức.
|
Đúng là |
Hiện tại, lời gọi ma thuật này là tất cả những gì bạn cần biết về lệnh git reset.
Chúng ta sẽ đi sâu hơn vào chi tiết về những gì reset làm và cách làm chủ nó để làm những điều thực sự thú vị trong [_git_reset].
Hủy sửa đổi một Tệp đã Sửa đổi
Điều gì sẽ xảy ra nếu bạn nhận ra rằng bạn không muốn giữ các thay đổi của mình đối với tệp CONTRIBUTING.md?
Làm thế nào bạn có thể dễ dàng hủy sửa đổi nó — hoàn nguyên nó về trạng thái như khi bạn cam kết lần cuối (hoặc sao chép ban đầu, hoặc bất cứ cách nào bạn đưa nó vào thư mục làm việc của mình)?
May mắn thay, git status cũng cho bạn biết cách làm điều đó.
Trong đầu ra ví dụ cuối cùng, khu vực chưa được tổ chức trông giống như thế này:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
Nó cho bạn biết khá rõ ràng cách loại bỏ các thay đổi bạn đã thực hiện. Hãy làm những gì nó nói:
$ git checkout -- CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
Bạn có thể thấy rằng các thay đổi đã được hoàn nguyên.
|
Điều quan trọng là phải hiểu rằng |
Nếu bạn muốn giữ các thay đổi bạn đã thực hiện đối với tệp đó nhưng vẫn cần loại bỏ nó ngay bây giờ, chúng ta sẽ xem xét việc stash và phân nhánh trong Nhánh trong Git; đây thường là những cách tốt hơn để đi.
Hãy nhớ rằng, bất cứ điều gì được cam kết trong Git hầu như luôn có thể được phục hồi.
Ngay cả các cam kết trên các nhánh đã bị xóa hoặc các cam kết bị ghi đè bằng cam kết --amend cũng có thể được phục hồi (xem [_data_recovery] để phục hồi dữ liệu).
Tuy nhiên, bất cứ điều gì bạn mất mà chưa bao giờ được cam kết có khả năng không bao giờ được nhìn thấy nữa.
Hoàn tác mọi thứ với git restore
Git phiên bản 2.23.0 đã giới thiệu một lệnh mới: git restore.
Về cơ bản, nó là một sự thay thế cho git reset mà chúng ta vừa đề cập.
Từ phiên bản Git 2.23.0 trở đi, Git sẽ sử dụng git restore thay vì git reset cho nhiều thao tác hoàn tác.
Hãy quay lại các bước của chúng ta và hoàn tác mọi thứ với git restore thay vì git reset.
Hủy tổ chức một Tệp đã Tổ chức với git restore
Hai phần tiếp theo minh họa cách làm việc với khu vực tổ chức và các thay đổi thư mục làm việc của bạn với git restore.
Phần hay là lệnh bạn sử dụng để xác định trạng thái của hai khu vực đó cũng nhắc nhở bạn cách hoàn tác các thay đổi đối với chúng.
Ví dụ: giả sử bạn đã thay đổi hai tệp và muốn cam kết chúng dưới dạng hai thay đổi riêng biệt, nhưng bạn vô tình nhập git add * và tổ chức cả hai.
Làm thế nào bạn có thể hủy tổ chức một trong hai?
Lệnh git status nhắc nhở bạn:
$ git add *
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: CONTRIBUTING.md
renamed: README.md -> README
Ngay bên dưới văn bản “Changes to be committed”, nó nói sử dụng git restore --staged <file>… để hủy tổ chức.
Vì vậy, hãy sử dụng lời khuyên đó để hủy tổ chức tệp CONTRIBUTING.md:
$ git restore --staged CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: README.md -> README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
Tệp CONTRIBUTING.md được sửa đổi nhưng một lần nữa không được tổ chức.
Hủy sửa đổi một Tệp đã Sửa đổi với git restore
Điều gì sẽ xảy ra nếu bạn nhận ra rằng bạn không muốn giữ các thay đổi của mình đối với tệp CONTRIBUTING.md?
Làm thế nào bạn có thể dễ dàng hủy sửa đổi nó — hoàn nguyên nó về trạng thái như khi bạn cam kết lần cuối (hoặc sao chép ban đầu, hoặc bất cứ cách nào bạn đưa nó vào thư mục làm việc của mình)?
May mắn thay, git status cũng cho bạn biết cách làm điều đó.
Trong đầu ra ví dụ cuối cùng, khu vực chưa được tổ chức trông giống như thế này:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
Nó cho bạn biết khá rõ ràng cách loại bỏ các thay đổi bạn đã thực hiện. Hãy làm những gì nó nói:
$ git restore CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: README.md -> README
|
Điều quan trọng là phải hiểu rằng |
Làm việc với Remotes
Để có thể cộng tác trên bất kỳ dự án Git nào, bạn cần biết cách quản lý các kho lưu trữ từ xa của mình. Kho lưu trữ từ xa là các phiên bản của dự án của bạn được lưu trữ trên Internet hoặc mạng ở đâu đó. Bạn có thể có một số trong số chúng, mỗi cái thường chỉ đọc hoặc đọc/ghi cho bạn. Cộng tác với người khác liên quan đến việc quản lý các kho lưu trữ từ xa này và đẩy và kéo dữ liệu đến và từ chúng khi bạn cần chia sẻ công việc. Quản lý kho lưu trữ từ xa bao gồm biết cách thêm kho lưu trữ từ xa, xóa các remote không còn hợp lệ, quản lý các nhánh từ xa khác nhau và xác định chúng là được theo dõi hay không, và hơn thế nữa. Trong phần này, chúng ta sẽ đề cập đến một số kỹ năng quản lý từ xa này.
|
Kho lưu trữ từ xa có thể nằm trên máy cục bộ của bạn.
Hoàn toàn có thể bạn có thể làm việc với một kho lưu trữ “từ xa” trên thực tế nằm trên cùng một máy chủ với bạn. Từ “từ xa” không nhất thiết ngụ ý rằng kho lưu trữ ở nơi khác trên mạng hoặc Internet, chỉ là nó ở nơi khác. Làm việc với một kho lưu trữ từ xa như vậy vẫn sẽ liên quan đến tất cả các thao tác đẩy, kéo và lấy tiêu chuẩn như với bất kỳ remote nào khác. |
Hiển thị Remotes của Bạn
Để xem bạn đã cấu hình máy chủ từ xa nào, bạn có thể chạy lệnh git remote.
Nó liệt kê các tên ngắn của mỗi remote handle bạn đã chỉ định.
Nếu bạn đã sao chép kho lưu trữ của mình, bạn ít nhất sẽ thấy origin — đó là tên mặc định mà Git đặt cho máy chủ bạn đã sao chép từ đó:
$ git clone https://github.com/schacon/ticgit
Cloning into 'ticgit'...
remote: Reusing existing pack: 1857, done.
remote: Total 1857 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (1857/1857), 374.35 KiB | 268.00 KiB/s, done.
Resolving deltas: 100% (772/772), done.
Checking connectivity... done.
$ cd ticgit
$ git remote
origin
Bạn cũng có thể chỉ định -v, hiển thị cho bạn các URL mà Git đã lưu trữ cho tên ngắn để được sử dụng khi đọc và ghi vào remote đó:
$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
Nếu bạn có nhiều hơn một remote, lệnh sẽ liệt kê tất cả chúng. Ví dụ, một kho lưu trữ với nhiều remote để làm việc với một số cộng tác viên có thể trông giống như thế này.
$ cd grit
$ git remote -v
bakkdoor https://github.com/bakkdoor/grit (fetch)
bakkdoor https://github.com/bakkdoor/grit (push)
cho45 https://github.com/cho45/grit (fetch)
cho45 https://github.com/cho45/grit (push)
defunkt https://github.com/defunkt/grit (fetch)
defunkt https://github.com/defunkt/grit (push)
koke git://github.com/koke/grit.git (fetch)
koke git://github.com/koke/grit.git (push)
origin git@github.com:mojombo/grit.git (fetch)
origin git@github.com:mojombo/grit.git (push)
Điều này có nghĩa là chúng ta có thể kéo đóng góp từ bất kỳ người dùng nào trong số này khá dễ dàng. Chúng ta có thể có thêm quyền đẩy lên một hoặc nhiều trong số này, mặc dù chúng ta không thể biết điều đó ở đây.
Lưu ý rằng các remote này sử dụng nhiều giao thức khác nhau; chúng ta sẽ đề cập thêm về điều này trong [_getting_git_on_a_server].
Thêm Kho lưu trữ Từ xa
Chúng tôi đã đề cập và đưa ra một số minh họa về cách lệnh git clone ngầm thêm remote origin cho bạn.
Đây là cách thêm một remote mới một cách rõ ràng.
Để thêm một kho lưu trữ Git từ xa mới làm tên ngắn bạn có thể tham chiếu dễ dàng, hãy chạy git remote add <shortname> <url>:
$ git remote
origin
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
pb https://github.com/paulboone/ticgit (fetch)
pb https://github.com/paulboone/ticgit (push)
Bây giờ bạn có thể sử dụng chuỗi pb trên dòng lệnh thay vì toàn bộ URL.
Ví dụ, nếu bạn muốn lấy tất cả thông tin mà Paul có nhưng bạn chưa có trong kho lưu trữ của mình, bạn có thể chạy git fetch pb:
$ git fetch pb
remote: Counting objects: 43, done.
remote: Compressing objects: 100% (36/36), done.
remote: Total 43 (delta 10), reused 31 (delta 5)
Unpacking objects: 100% (43/43), done.
From https://github.com/paulboone/ticgit
* [new branch] master -> pb/master
* [new branch] ticgit -> pb/ticgit
Nhánh master của Paul bây giờ có thể truy cập cục bộ dưới dạng pb/master — bạn có thể hợp nhất nó vào một trong các nhánh của mình, hoặc bạn có thể kiểm xuất một nhánh cục bộ tại thời điểm đó nếu bạn muốn kiểm tra nó.
Chúng ta sẽ xem xét các nhánh là gì và cách sử dụng chúng chi tiết hơn nhiều trong Nhánh trong Git.
Lấy và Kéo từ Remotes của Bạn
Như bạn vừa thấy, để lấy dữ liệu từ các dự án từ xa của bạn, bạn có thể chạy:
$ git fetch <remote>
Lệnh này đi ra dự án từ xa đó và kéo xuống tất cả dữ liệu từ dự án từ xa đó mà bạn chưa có. Sau khi bạn làm điều này, bạn sẽ có tham chiếu đến tất cả các nhánh từ remote đó, mà bạn có thể hợp nhất hoặc kiểm tra bất cứ lúc nào.
Nếu bạn sao chép một kho lưu trữ, lệnh sẽ tự động thêm kho lưu trữ từ xa đó dưới tên “origin”.
Vì vậy, git fetch origin lấy bất kỳ công việc mới nào đã được đẩy lên máy chủ đó kể từ khi bạn sao chép (hoặc lần cuối lấy từ) nó.
Điều quan trọng cần lưu ý là lệnh git fetch chỉ tải xuống dữ liệu vào kho lưu trữ cục bộ của bạn — nó không tự động hợp nhất nó với bất kỳ công việc nào của bạn hoặc sửa đổi những gì bạn đang làm việc hiện tại.
Bạn phải hợp nhất nó theo cách thủ công vào công việc của mình khi bạn sẵn sàng.
Nếu nhánh hiện tại của bạn được thiết lập để theo dõi một nhánh từ xa (xem phần tiếp theo và Nhánh trong Git để biết thêm thông tin), bạn có thể sử dụng lệnh git pull để tự động lấy và sau đó hợp nhất nhánh từ xa đó vào nhánh hiện tại của bạn.
Đây có thể là một quy trình làm việc dễ dàng hơn hoặc thoải mái hơn cho bạn; và theo mặc định, lệnh git clone tự động thiết lập nhánh master cục bộ của bạn để theo dõi nhánh master từ xa (hoặc bất kỳ tên nhánh mặc định nào được gọi) trên máy chủ bạn đã sao chép từ đó.
Chạy git pull thường lấy dữ liệu từ máy chủ bạn ban đầu sao chép từ đó và tự động cố gắng hợp nhất nó vào mã bạn đang làm việc hiện tại.
|
Từ phiên bản Git 2.27 trở đi, Nếu bạn muốn hành vi mặc định của Git (fast-forward nếu có thể, nếu không tạo một commit hợp nhất):
Nếu bạn muốn rebase khi kéo:
|
Đẩy lên Remotes của Bạn
Khi bạn có dự án của mình ở một điểm mà bạn muốn chia sẻ, bạn phải đẩy nó lên upstream.
Lệnh cho điều này rất đơn giản: git push <remote> <branch>.
Nếu bạn muốn đẩy nhánh master của mình lên máy chủ origin của bạn (một lần nữa, việc sao chép thường thiết lập cả hai tên đó cho bạn tự động), thì bạn có thể chạy lệnh này để đẩy bất kỳ commit nào bạn đã thực hiện trở lại máy chủ:
$ git push origin master
Lệnh này chỉ hoạt động nếu bạn sao chép từ một máy chủ mà bạn có quyền ghi và nếu không ai đã đẩy trong thời gian chờ đợi. Nếu bạn và người khác sao chép cùng một lúc và họ đẩy upstream và sau đó bạn đẩy upstream, việc đẩy của bạn sẽ bị từ chối một cách đúng đắn. Bạn sẽ phải lấy công việc của họ trước và kết hợp nó vào của bạn trước khi bạn được phép đẩy. Xem Nhánh trong Git để biết thêm thông tin chi tiết về cách đẩy lên máy chủ từ xa.
Kiểm tra một Remote
Nếu bạn muốn xem thêm thông tin về một remote cụ thể, bạn có thể sử dụng lệnh git remote show <remote>.
Nếu bạn chạy lệnh này với một tên ngắn cụ thể, chẳng hạn như origin, bạn sẽ nhận được một cái gì đó như thế này:
$ git remote show origin
* remote origin
Fetch URL: https://github.com/schacon/ticgit
Push URL: https://github.com/schacon/ticgit
HEAD branch: master
Remote branches:
master tracked
dev-branch tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (up to date)
Nó liệt kê URL cho kho lưu trữ từ xa cũng như thông tin nhánh theo dõi.
Lệnh này hữu ích cho bạn biết rằng nếu bạn đang ở trên nhánh master và bạn chạy git pull, nó sẽ tự động hợp nhất nhánh master của remote vào nhánh cục bộ sau khi nó đã được lấy.
Nó cũng liệt kê tất cả các tham chiếu từ xa mà nó đã kéo xuống.
Đó là một ví dụ đơn giản mà bạn có thể gặp phải.
Tuy nhiên, khi bạn sử dụng Git nhiều hơn, bạn có thể thấy nhiều thông tin hơn từ git remote show:
$ git remote show origin
* remote origin
URL: https://github.com/my-org/complex-project
Fetch URL: https://github.com/my-org/complex-project
Push URL: https://github.com/my-org/complex-project
HEAD branch: master
Remote branches:
master tracked
dev-branch tracked
markdown-strip tracked
issue-43 new (next fetch will store in remotes/origin)
issue-45 new (next fetch will store in remotes/origin)
refs/remotes/origin/issue-11 stale (use 'git remote prune' to remove)
Local branches configured for 'git pull':
dev-branch merges with remote dev-branch
master merges with remote master
Local refs configured for 'git push':
dev-branch pushes to dev-branch (up to date)
markdown-strip pushes to markdown-strip (up to date)
master pushes to master (up to date)
Lệnh này hiển thị nhánh nào được tự động đẩy lên khi bạn chạy git push trong khi ở trên các nhánh nhất định.
Nó cũng cho bạn thấy những nhánh từ xa nào trên máy chủ mà bạn chưa có, những nhánh từ xa nào bạn có đã bị xóa khỏi máy chủ và nhiều nhánh cục bộ có thể tự động hợp nhất với nhánh theo dõi từ xa của chúng khi bạn chạy git pull.
Đổi tên và Xóa Remotes
Bạn có thể chạy git remote rename để thay đổi tên ngắn của một remote.
Ví dụ, nếu bạn muốn đổi tên pb thành paul, bạn có thể làm như vậy với git remote rename:
$ git remote rename pb paul
$ git remote
origin
paul
Đáng chú ý là điều này cũng thay đổi tất cả tên nhánh theo dõi từ xa của bạn.
Những gì từng được tham chiếu tại pb/master bây giờ ở paul/master.
Nếu bạn muốn xóa một remote vì lý do nào đó — bạn đã di chuyển máy chủ hoặc không còn sử dụng một mirror cụ thể, hoặc có lẽ một người đóng góp không còn đóng góp nữa — bạn có thể sử dụng git remote remove hoặc git remote rm:
$ git remote remove paul
$ git remote
origin
Khi bạn xóa tham chiếu đến một remote theo cách này, tất cả các nhánh theo dõi từ xa và cài đặt cấu hình liên quan đến remote đó cũng bị xóa.
Gắn Thẻ
Giống như hầu hết các VCS, Git có khả năng gắn thẻ các điểm cụ thể trong lịch sử của kho lưu trữ là quan trọng.
Thông thường, mọi người sử dụng chức năng này để đánh dấu các điểm phát hành (v1.0, v2.0 và như vậy).
Trong phần này, bạn sẽ học cách liệt kê các thẻ hiện có, cách tạo và xóa thẻ và các loại thẻ khác nhau là gì.
Liệt kê Thẻ của Bạn
Liệt kê các thẻ hiện có trong Git rất đơn giản.
Chỉ cần nhập git tag (với tùy chọn -l hoặc --list):
$ git tag
v1.0
v2.0
Lệnh này liệt kê các thẻ theo thứ tự bảng chữ cái; thứ tự chúng được hiển thị không có tầm quan trọng thực sự.
Bạn cũng có thể tìm kiếm các thẻ khớp với một mẫu cụ thể. Ví dụ, kho nguồn Git chứa hơn 500 thẻ. Nếu bạn chỉ quan tâm đến việc xem chuỗi 1.8.5, bạn có thể chạy lệnh này:
$ git tag -l "v1.8.5*"
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
v1.8.5-rc2
v1.8.5-rc3
v1.8.5.1
v1.8.5.2
v1.8.5.3
v1.8.5.4
v1.8.5.5
|
Liệt kê ký tự đại diện thẻ yêu cầu tùy chọn
-l hoặc --listNếu bạn chỉ muốn toàn bộ danh sách các thẻ, chạy lệnh Tuy nhiên, nếu bạn đang cung cấp một mẫu ký tự đại diện để khớp tên thẻ, việc sử dụng |
Tạo Thẻ
Git hỗ trợ hai loại thẻ: lightweight (nhẹ) và annotated (chú thích).
Thẻ nhẹ rất giống một nhánh không thay đổi — nó chỉ là một con trỏ đến một commit cụ thể.
Tuy nhiên, các thẻ chú thích được lưu trữ dưới dạng các đối tượng đầy đủ trong cơ sở dữ liệu Git. Chúng được kiểm tra tổng kiểm tra; chứa tên người gắn thẻ, email và ngày; có thông điệp gắn thẻ; và có thể được ký và xác minh bằng GNU Privacy Guard (GPG). Thường được khuyến nghị rằng bạn tạo các thẻ chú thích để bạn có thể có tất cả thông tin này; nhưng nếu bạn muốn một thẻ tạm thời hoặc vì lý do nào đó không muốn giữ thông tin khác, các thẻ nhẹ cũng có sẵn.
Thẻ Chú thích
Tạo một thẻ chú thích trong Git rất đơn giản.
Cách dễ nhất là chỉ định -a khi bạn chạy lệnh tag:
$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.1
v1.3
v1.4
-m chỉ định một thông điệp gắn thẻ, được lưu trữ cùng với thẻ.
Nếu bạn không chỉ định thông điệp cho một thẻ chú thích, Git sẽ khởi chạy trình soạn thảo của bạn để bạn có thể nhập nó vào.
Bạn có thể thấy dữ liệu thẻ cùng với commit đã được gắn thẻ bằng cách sử dụng lệnh git show:
$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date: Sat May 3 20:19:12 2014 -0700
my version 1.4
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
Điều đó hiển thị thông tin người gắn thẻ, ngày commit được gắn thẻ và thông điệp chú thích trước khi hiển thị thông tin commit.
Thẻ Nhẹ
Một cách khác để gắn thẻ commit là với một thẻ nhẹ.
Về cơ bản đây là tổng kiểm tra commit được lưu trữ trong một tệp — không có thông tin khác được giữ lại.
Để tạo một thẻ nhẹ, đừng cung cấp bất kỳ tùy chọn -a, -s hoặc -m nào, chỉ cung cấp tên thẻ:
$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5
Lần này, nếu bạn chạy git show trên thẻ, bạn không thấy thông tin thẻ bổ sung.
Lệnh chỉ hiển thị commit:
$ git show v1.4-lw
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
Gắn Thẻ Sau
Bạn cũng có thể gắn thẻ commit sau khi bạn đã di chuyển qua chúng. Giả sử lịch sử commit của bạn trông như thế này:
$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment'
a6b4c97498bd301d84096da251c98a07c7723e65 Create write support
0d52aaab4479697da7686c15f77a3d64d9165190 One more thing
6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment'
0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc Add commit function
4682c3261057305bdd616e23b64b0857d832627b Add todo file
166ae0c4d3f420721acbb115cc33848dfcc2121a Create write support
9fceb02d0ae598e95dc970b74767f19372d61af8 Update rakefile
964f16d36dfccde844893cac5b347e7b3d44abbc Commit the todo
8a5cbc430f1a9c3d00faaeffd07798508422908a Update readme
Bây giờ, giả sử bạn quên gắn thẻ dự án tại v1.2, đó là tại commit “Update rakefile”. Bạn có thể thêm nó sau. Để gắn thẻ commit đó, bạn chỉ định tổng kiểm tra commit (hoặc một phần của nó) ở cuối lệnh:
$ git tag -a v1.2 9fceb02
Bạn có thể thấy rằng bạn đã gắn thẻ commit:
$ git tag
v0.1
v1.2
v1.3
v1.4
v1.4-lw
v1.5
$ git show v1.2
tag v1.2
Tagger: Scott Chacon <schacon@gee-mail.com>
Date: Mon Feb 9 15:32:16 2009 -0800
version 1.2
commit 9fceb02d0ae598e95dc970b74767f19372d61af8
Author: Magnus Chacon <mchacon@gee-mail.com>
Date: Sun Apr 27 20:43:35 2008 -0700
Update rakefile
...
Chia sẻ Thẻ
Theo mặc định, lệnh git push không chuyển thẻ đến máy chủ từ xa.
Bạn sẽ phải đẩy thẻ một cách rõ ràng lên máy chủ chia sẻ sau khi bạn đã tạo chúng.
Quá trình này giống như chia sẻ các nhánh từ xa — bạn có thể chạy git push origin <tagname>.
$ git push origin v1.5
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new tag] v1.5 -> v1.5
Nếu bạn có nhiều thẻ mà bạn muốn đẩy lên cùng một lúc, bạn cũng có thể sử dụng tùy chọn --tags cho lệnh git push.
Điều này sẽ chuyển tất cả các thẻ của bạn lên máy chủ từ xa mà chưa có ở đó.
$ git push origin --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 160 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new tag] v1.4 -> v1.4
* [new tag] v1.4-lw -> v1.4-lw
Bây giờ, khi người khác sao chép hoặc kéo từ kho lưu trữ của bạn, họ cũng sẽ nhận được tất cả các thẻ của bạn.
git push đẩy cả hai loại thẻ
|
Xóa Thẻ
Để xóa một thẻ trên kho lưu trữ cục bộ của bạn, bạn có thể sử dụng git tag -d <tagname>.
Ví dụ, chúng ta có thể xóa thẻ nhẹ của mình ở trên như sau:
$ git tag -d v1.4-lw
Deleted tag 'v1.4-lw' (was e7d5add)
Lưu ý rằng điều này không xóa thẻ khỏi bất kỳ máy chủ từ xa nào. Có hai biến thể phổ biến để xóa một thẻ khỏi máy chủ từ xa.
Biến thể đầu tiên là git push <remote> :refs/tags/<tagname>:
$ git push origin :refs/tags/v1.4-lw
To /git@github.com:schacon/simplegit.git
- [deleted] v1.4-lw
Cách để hiểu điều trên là đọc nó như giá trị null trước dấu hai chấm đang được đẩy lên tên thẻ từ xa, xóa nó một cách hiệu quả.
Cách thứ hai (và trực quan hơn) để xóa một thẻ từ xa là với:
$ git push origin --delete <tagname>
Kiểm xuất Thẻ
Nếu bạn muốn xem các phiên bản của tệp mà một thẻ đang trỏ đến, bạn có thể thực hiện git checkout của thẻ đó, mặc dù điều này đặt kho lưu trữ của bạn vào trạng thái “detached HEAD”, có một số tác dụng phụ không tốt:
$ git checkout v2.0.0
Note: switching to 'v2.0.0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 99ada87... Merge pull request #89 from schacon/appendix-final
$ git checkout v2.0-beta-0.1
Previous HEAD position was 99ada87... Merge pull request #89 from schacon/appendix-final
HEAD is now at df3f601... Add atlas.json and cover image
Trong trạng thái “detached HEAD”, nếu bạn thực hiện thay đổi và sau đó tạo một commit, thẻ sẽ giữ nguyên, nhưng commit mới của bạn sẽ không thuộc về bất kỳ nhánh nào và sẽ không thể truy cập được, ngoại trừ bằng hash commit chính xác. Do đó, nếu bạn cần thực hiện thay đổi — giả sử bạn đang sửa lỗi trên một phiên bản cũ hơn, chẳng hạn — bạn thường sẽ muốn tạo một nhánh:
$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'
Nếu bạn làm điều này và thực hiện một commit, nhánh version2 của bạn sẽ hơi khác so với thẻ v2.0.0 của bạn vì nó sẽ tiến lên với các thay đổi mới của bạn, vì vậy hãy cẩn thận.
Bí danh Git
Trước khi chúng ta chuyển sang chương tiếp theo, chúng tôi muốn giới thiệu một tính năng có thể làm cho trải nghiệm Git của bạn đơn giản hơn, dễ dàng hơn và quen thuộc hơn: bí danh. Để rõ ràng, chúng tôi sẽ không sử dụng chúng ở bất cứ nơi nào khác trong cuốn sách này, nhưng nếu bạn tiếp tục sử dụng Git với bất kỳ tính đều đặn nào, bí danh là thứ bạn nên biết.
Git không tự động suy ra lệnh của bạn nếu bạn nhập nó một phần.
Nếu bạn không muốn nhập toàn bộ văn bản của mỗi lệnh Git, bạn có thể dễ dàng thiết lập một bí danh cho mỗi lệnh bằng cách sử dụng git config.
Dưới đây là một vài ví dụ bạn có thể muốn thiết lập:
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status
Điều này có nghĩa là, ví dụ, thay vì nhập git commit, bạn chỉ cần nhập git ci.
Khi bạn tiếp tục sử dụng Git, bạn có thể cũng sẽ sử dụng các lệnh khác thường xuyên; đừng ngần ngại tạo bí danh mới.
Kỹ thuật này cũng có thể rất hữu ích trong việc tạo các lệnh mà bạn nghĩ nên tồn tại. Ví dụ, để sửa chữa vấn đề khả năng sử dụng bạn gặp phải với việc hủy tổ chức một tệp, bạn có thể thêm bí danh unstage của riêng bạn vào Git:
$ git config --global alias.unstage 'reset HEAD --'
Điều này làm cho hai lệnh sau tương đương:
$ git unstage fileA
$ git reset HEAD -- fileA
Điều này có vẻ rõ ràng hơn một chút.
Cũng phổ biến khi thêm một lệnh last, như thế này:
$ git config --global alias.last 'log -1 HEAD'
Bằng cách này, bạn có thể xem cam kết cuối cùng một cách dễ dàng:
$ git last
commit 66938dae3329c7aebe598c2246a8e6af90d04646
Author: Josh Goebel <dreamer3@example.com>
Date: Tue Aug 26 19:48:51 2008 +0800
Test for current head
Signed-off-by: Scott Chacon <schacon@example.com>
Như bạn có thể thấy, Git chỉ đơn giản thay thế lệnh mới bằng bất cứ thứ gì bạn đặt bí danh cho nó.
Tuy nhiên, có thể bạn muốn chạy một lệnh bên ngoài, thay vì một lệnh con Git.
Trong trường hợp đó, bạn bắt đầu lệnh bằng ký tự !.
Điều này hữu ích nếu bạn viết các công cụ của riêng mình hoạt động với kho lưu trữ Git.
Chúng ta có thể chứng minh bằng cách đặt bí danh git visual để chạy gitk:
$ git config --global alias.visual '!gitk'
Tóm tắt
Ở giai đoạn này, bạn có thể thực hiện tất cả các thao tác Git cục bộ cơ bản — tạo hoặc clone một kho, thực hiện thay đổi, stage và commit các thay đổi đó, và xem lịch sử toàn bộ thay đổi của kho. Tiếp theo, chúng ta sẽ tìm hiểu tính năng nổi bật nhất của Git: mô hình nhánh của nó.
Nhánh trong Git
Hầu như mọi hệ thống quản lý phiên bản (VCS) đều hỗ trợ một dạng nhánh nào đó. Nhánh có nghĩa là bạn tách ra khỏi dòng phát triển chính và tiếp tục làm việc mà không làm ảnh hưởng tới dòng chính đó. Trong nhiều VCS, thao tác này khá tốn kém, thường yêu cầu tạo bản sao mới của thư mục mã nguồn, điều này có thể mất nhiều thời gian cho các dự án lớn.
Một số người gọi mô hình nhánh của Git là "tính năng kinh điển" của nó, và thực sự nó khiến Git khác biệt trong cộng đồng VCS. Tại sao nó đặc biệt? Cách Git quản lý nhánh rất nhẹ, giúp thao tác tạo nhánh gần như tức thời, và việc chuyển đổi giữa các nhánh thường cũng rất nhanh. Không giống nhiều VCS khác, Git khuyến khích các luồng làm việc tạo nhánh và hợp nhất thường xuyên, thậm chí nhiều lần trong ngày. Hiểu và làm chủ tính năng này sẽ cung cấp cho bạn một công cụ mạnh mẽ và độc đáo, có thể thay đổi hoàn toàn cách bạn phát triển phần mềm.
Các Nhánh trong Git
Để thực sự hiểu cách Git thực hiện phân nhánh, chúng ta cần lùi lại một bước và kiểm tra cách Git lưu trữ dữ liệu của nó.
Như bạn có thể nhớ lại từ ch01-introduction.html, Git không lưu trữ dữ liệu dưới dạng một loạt các thay đổi hoặc khác biệt, mà thay vào đó là một loạt các ảnh chụp nhanh (snapshots).
Khi bạn thực hiện một cam kết, Git lưu trữ một đối tượng cam kết chứa một con trỏ đến ảnh chụp nhanh của nội dung bạn đã tổ chức. Đối tượng này cũng chứa tên và email của tác giả, thông điệp bạn đã nhập và các con trỏ đến cam kết hoặc các cam kết trực tiếp trước cam kết này (cha của nó): không có cha cho cam kết ban đầu, một cha cho một cam kết bình thường và nhiều cha cho một cam kết là kết quả của việc hợp nhất hai hoặc nhiều nhánh.
Để hình dung điều này, hãy giả sử rằng bạn có một thư mục chứa ba tệp và bạn tổ chức tất cả chúng và cam kết. Việc tổ chức các tệp sẽ tính toán tổng kiểm tra cho mỗi tệp (băm SHA-1 mà chúng ta đã đề cập trong ch01-introduction.html), lưu trữ phiên bản tệp đó trong kho lưu trữ Git (Git gọi chúng là blobs) và thêm tổng kiểm tra đó vào khu vực tổ chức:
$ git add README test.rb LICENSE
$ git commit -m 'Initial commit'
Khi bạn tạo cam kết bằng cách chạy git commit, Git tính tổng kiểm tra từng thư mục con (trong trường hợp này, chỉ thư mục dự án gốc) và lưu trữ chúng dưới dạng một đối tượng cây trong kho lưu trữ Git.
Sau đó, Git tạo một đối tượng cam kết có siêu dữ liệu và một con trỏ đến cây dự án gốc để nó có thể tạo lại ảnh chụp nhanh đó khi cần.
Kho lưu trữ Git của bạn hiện chứa năm đối tượng: ba blobs (mỗi tệp một blob), một tree liệt kê nội dung của thư mục và chỉ định tên tệp nào được lưu trữ dưới dạng blob nào, và một commit với con trỏ đến cây gốc đó và tất cả siêu dữ liệu cam kết.
Nếu bạn thực hiện một số thay đổi và cam kết lại, cam kết tiếp theo sẽ lưu trữ một con trỏ đến cam kết đến ngay trước nó.
Một nhánh trong Git chỉ đơn giản là một con trỏ di động nhẹ đến một trong những cam kết này.
Tên nhánh mặc định trong Git là master.
Khi bạn bắt đầu thực hiện các cam kết, bạn được cung cấp một nhánh master trỏ đến cam kết cuối cùng bạn đã thực hiện.
Mỗi lần bạn cam kết, con trỏ nhánh master sẽ tự động di chuyển về phía trước.
|
Nhánh “master” trong Git không phải là một nhánh đặc biệt.
Nó hoàn toàn giống như bất kỳ nhánh nào khác.
Lý do duy nhất khiến gần như mọi kho lưu trữ đều có một nhánh là lệnh |
Tạo một Nhánh Mới
Điều gì xảy ra khi bạn tạo một nhánh mới?
Làm như vậy sẽ tạo ra một con trỏ mới để bạn di chuyển xung quanh.
Giả sử bạn muốn tạo một nhánh mới có tên là testing.
Bạn làm điều này với lệnh git branch:
$ git branch testing
Điều này tạo ra một con trỏ mới đến cùng một cam kết mà bạn đang ở hiện tại.
Làm thế nào để Git biết bạn đang ở nhánh nào?
Nó giữ một con trỏ đặc biệt gọi là HEAD.
Lưu ý rằng điều này khác rất nhiều so với khái niệm HEAD trong các VCS khác mà bạn có thể đã quen thuộc, chẳng hạn như Subversion hoặc CVS.
Trong Git, đây là một con trỏ đến nhánh cục bộ mà bạn đang ở hiện tại.
Trong trường hợp này, bạn vẫn đang ở trên master.
Lệnh git branch chỉ tạo một nhánh mới — nó không chuyển sang nhánh đó.
Bạn có thể dễ dàng thấy điều này bằng cách chạy một lệnh git log đơn giản hiển thị cho bạn nơi các con trỏ nhánh đang trỏ đến.
Tùy chọn này được gọi là --decorate.
$ git log --oneline --decorate
f30ab (HEAD -> master, testing) Add feature #32 - ability to add new formats to the central interface
34ac2 Fix bug #1328 - stack overflow under certain conditions
98ca9 Initial commit of my project
Bạn có thể thấy các nhánh master và testing nằm ngay cạnh cam kết f30ab.
Chuyển Nhánh
Để chuyển sang một nhánh hiện có, bạn chạy lệnh git checkout.
Hãy chuyển sang nhánh testing mới:
$ git checkout testing
Điều này di chuyển HEAD để trỏ đến nhánh testing.
Ý nghĩa của việc đó là gì? Vâng, hãy thực hiện một cam kết khác:
$ vim test.rb
$ git commit -a -m 'made a change'
Điều này thật thú vị, bởi vì nhánh testing của bạn đã di chuyển về phía trước, nhưng nhánh master của bạn vẫn trỏ đến cam kết bạn đã ở khi bạn chạy git checkout để chuyển đổi các nhánh.
Hãy chuyển trở lại nhánh master:
$ git checkout master
git log không hiển thị tất cả các nhánh mọi lúcNếu bạn chạy Nhánh đó không biến mất; Git chỉ không biết rằng bạn quan tâm đến nhánh đó.
Những gì |
Lệnh đó đã làm hai việc.
Nó di chuyển con trỏ HEAD trở lại trỏ vào nhánh master, và nó hoàn nguyên các tệp trong thư mục làm việc của bạn trở lại ảnh chụp nhanh mà master trỏ đến.
Điều này cũng có nghĩa là những thay đổi bạn thực hiện từ thời điểm này trở đi sẽ tách ra khỏi phiên bản cũ hơn của dự án.
Về cơ bản, nó tua lại công việc bạn đã làm trong nhánh testing của mình để bạn có thể đi theo một hướng khác.
|
Điều quan trọng cần lưu ý là khi bạn chuyển đổi các nhánh trong Git, các tệp trong thư mục làm việc của bạn sẽ thay đổi. Nếu bạn chuyển sang một nhánh cũ hơn, thư mục làm việc của bạn sẽ được hoàn nguyên để trông giống như lần cuối cùng bạn cam kết trên nhánh đó. Nếu Git không thể làm điều đó một cách sạch sẽ, nó sẽ không cho phép bạn chuyển đổi chút nào. |
Hãy thực hiện một vài thay đổi và cam kết lại:
$ vim test.rb
$ git commit -a -m 'Make other changes'
Bây giờ lịch sử dự án của bạn đã phân kỳ (xem Lịch sử phân kỳ).
Bạn đã tạo và chuyển sang một nhánh, thực hiện một số công việc trên đó, và sau đó chuyển trở lại nhánh chính của mình và thực hiện công việc khác.
Cả hai thay đổi đó đều được tách biệt trong các nhánh riêng biệt: bạn có thể chuyển đổi qua lại giữa hai nhánh và hợp nhất chúng lại với nhau khi bạn sẵn sàng.
Và bạn đã làm tất cả những điều đó với các lệnh branch, checkout và commit đơn giản.
Bạn cũng có thể thấy điều này dễ dàng với lệnh git log.
Nếu bạn chạy git log --oneline --decorate --graph --all, nó sẽ in ra lịch sử cam kết của bạn, hiển thị vị trí các con trỏ nhánh của bạn và lịch sử đã phân kỳ như thế nào.
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD -> master) Make other changes
| * 87ab2 (testing) made a change
|/
* f30ab Add feature #32 - ability to add new formats to the central interface
* 34ac2 Fix bug #1328 - stack overflow under certain conditions
* 98ca9 Initial commit of my project
Bởi vì một nhánh trong Git thực sự là một tệp đơn giản chứa tổng kiểm tra SHA-1 gồm 40 ký tự của cam kết mà nó trỏ đến, các nhánh rất rẻ để tạo và hủy. Tạo một nhánh mới nhanh chóng và đơn giản như viết 41 byte vào một tệp (40 ký tự và một dòng mới).
Điều này hoàn toàn trái ngược với cách hầu hết các công cụ VCS cũ hơn phân nhánh, liên quan đến việc sao chép tất cả các tệp của dự án vào thư mục thứ hai. Điều này có thể mất vài giây hoặc thậm chí vài phút, tùy thuộc vào kích thước của dự án, trong khi trong Git, quá trình này luôn diễn ra ngay lập tức. Ngoài ra, vì chúng ta đang ghi lại các cha mẹ khi chúng ta cam kết, việc tìm cơ sở hợp nhất tốt nhất để hợp nhất được thực hiện tự động cho chúng ta và nói chung là rất dễ thực hiện. Các tính năng này giúp khuyến khích các nhà phát triển tạo và sử dụng các nhánh thường xuyên.
Hãy xem tại sao bạn nên làm như vậy.
|
git switch
Từ phiên bản Git 2.23 trở đi, bạn có thể sử dụng
|
Hãy cùng xem một ví dụ đơn giản về phân nhánh và hợp nhất với một quy trình làm việc mà bạn có thể sử dụng trong thế giới thực. Bạn sẽ làm theo các bước sau:
-
Làm một số công việc trên một trang web.
-
Tạo một nhánh cho một câu chuyện người dùng mới mà bạn đang làm.
-
Làm một số công việc trong nhánh đó.
Ở giai đoạn này, bạn sẽ nhận được một cuộc gọi rằng một vấn đề khác là khẩn cấp và bạn cần một bản vá nóng. Bạn sẽ làm như sau:
-
Chuyển về nhánh sản xuất của bạn.
-
Tạo một nhánh để thêm bản vá nóng.
-
Sau khi đã được kiểm tra, hãy hợp nhất nhánh vá nóng và đẩy lên sản xuất.
-
Chuyển về câu chuyện người dùng ban đầu của bạn và tiếp tục làm việc.
Phân nhánh Cơ bản
Đầu tiên, giả sử bạn đang làm việc trên dự án của mình và đã có một vài cam kết trên nhánh master.
Bạn đã quyết định rằng bạn sẽ làm việc trên vấn đề #53 trong bất kỳ hệ thống theo dõi vấn đề nào mà công ty bạn sử dụng.
Để tạo một nhánh mới và chuyển sang nó cùng một lúc, bạn có thể chạy lệnh git checkout với một công tắc -b:
$ git checkout -b iss53
Switched to a new branch "iss53"
Đây là cách viết tắt cho:
$ git branch iss53
$ git checkout iss53
Bạn làm một số công việc trên trang web của mình và cam kết các thay đổi của bạn.
Làm như vậy sẽ di chuyển nhánh iss53 về phía trước, bởi vì bạn đã kiểm xuất nó (nghĩa là, HEAD của bạn đang trỏ đến nó):
$ vim index.html
$ git commit -a -m 'Thêm một chân trang mới [vấn đề 53]'
iss53 đã di chuyển về phía trước với công việc của bạnBây giờ bạn nhận được cuộc gọi rằng có một vấn đề với trang web và bạn cần phải sửa nó ngay lập tức.
Với Git, bạn không cần phải triển khai bản vá của mình cùng với các thay đổi iss53 bạn đã thực hiện, và bạn không cần phải nỗ lực nhiều để hoàn nguyên các thay đổi đó trước khi bạn có thể làm việc trên việc áp dụng bản vá của mình cho những gì đang có trong sản xuất.
Tất cả những gì bạn phải làm là chuyển về nhánh master của bạn.
Tuy nhiên, trước khi bạn làm điều đó, hãy lưu ý rằng nếu thư mục làm việc hoặc khu vực tổ chức của bạn có các thay đổi chưa được cam kết xung đột với nhánh bạn đang kiểm xuất, Git sẽ không cho phép bạn chuyển nhánh.
Tốt nhất là có một trạng thái làm việc sạch khi bạn chuyển nhánh.
Có nhiều cách để giải quyết vấn đề này (cụ thể là, cất giữ và sửa đổi cam kết) mà chúng ta sẽ đề cập sau này, trong [_git_stashing].
Hiện tại, giả sử bạn đã cam kết tất cả các thay đổi của mình, vì vậy bạn có thể chuyển về nhánh master của mình:
$ git checkout master
Switched to branch 'master'
Tại thời điểm này, thư mục làm việc dự án của bạn ở trạng thái chính xác như trước khi bạn bắt đầu làm việc trên vấn đề #53, và bạn có thể tập trung vào bản vá nóng của mình. Đây là một điểm quan trọng cần nhớ: khi bạn chuyển nhánh, Git đặt lại thư mục làm việc của bạn để trông giống như lần cuối cùng bạn cam kết trên nhánh đó. Nó tự động thêm, xóa và sửa đổi các tệp để đảm bảo bản sao làm việc của bạn là những gì nhánh trông như thế nào trong lần cam kết cuối cùng của bạn với nó.
Tiếp theo, bạn có một bản vá nóng để thực hiện. Hãy tạo một nhánh vá nóng để làm việc cho đến khi hoàn thành:
$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'Sửa địa chỉ email bị hỏng'
[hotfix 1fb7853] Sửa địa chỉ email bị hỏng
1 file changed, 2 insertions(+), 2 deletions(-)
Bạn có thể chạy các bài kiểm tra của mình, đảm bảo bản vá nóng là những gì bạn muốn, và sau đó hợp nhất nó trở lại vào nhánh master của bạn để triển khai vào sản xuất.
Bạn làm điều này với lệnh git merge:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
Bạn sẽ nhận thấy cụm từ “Fast-forward” trong lần hợp nhất đó.
Bởi vì cam kết C4 được trỏ đến bởi nhánh hotfix bạn đã hợp nhất vào trực tiếp ở phía trước của cam kết C2 bạn đang ở, Git chỉ đơn giản di chuyển con trỏ về phía trước.
Nói cách khác, khi bạn cố gắng hợp nhất một cam kết với một cam kết có thể đạt được bằng cách theo dõi lịch sử của cam kết đầu tiên, Git đơn giản hóa mọi thứ bằng cách di chuyển con trỏ về phía trước vì không có công việc phân kỳ để hợp nhất với nhau — điều này được gọi là “fast-forward”.
Thay đổi của bạn bây giờ nằm trong ảnh chụp nhanh của cam kết được trỏ đến bởi nhánh master, và bạn có thể triển khai bản vá của mình.
Sau khi bản vá nóng siêu quan trọng của bạn được triển khai, bạn đã sẵn sàng để chuyển về công việc bạn đang làm trước khi bị gián đoạn.
Tuy nhiên, trước tiên bạn sẽ xóa nhánh hotfix, bởi vì bạn không còn cần nó nữa — nhánh master trỏ đến cùng một nơi.
Bạn có thể xóa nó bằng tùy chọn -d cho git branch:
$ git branch -d hotfix
Deleted branch hotfix (was 3a0874c).
Bây giờ bạn có thể chuyển về nhánh đang trong quá trình làm việc cho vấn đề #53 và tiếp tục làm việc trên đó.
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'Hoàn thành vấn đề 53'
[iss53 348574c] Hoàn thành vấn đề 53
1 file changed, 1 insertion(+), 1 deletion(-)
Điều đáng chú ý ở đây là công việc bạn đã làm trong nhánh hotfix của bạn không được chứa trong các tệp trong nhánh iss53 của bạn.
Nếu bạn cần kéo nó vào, bạn có thể hợp nhất nhánh master của bạn vào nhánh iss53 của bạn bằng cách chạy git merge master, hoặc bạn có thể đợi để tích hợp các thay đổi đó cho đến khi bạn quyết định kéo nhánh iss53 trở lại vào master sau này.
Hợp nhất Cơ bản
Giả sử bạn đã quyết định rằng công việc vấn đề #53 của bạn đã hoàn thành và sẵn sàng để được hợp nhất vào nhánh master của bạn.
Để làm điều đó, bạn sẽ hợp nhất nhánh iss53 của bạn vào master, giống như bạn đã hợp nhất nhánh hotfix của mình trước đó.
Tất cả những gì bạn phải làm là kiểm xuất nhánh bạn muốn hợp nhất vào và sau đó chạy lệnh git merge:
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)
Điều này trông hơi khác so với việc hợp nhất hotfix bạn đã làm trước đó.
Trong trường hợp này, lịch sử phát triển của bạn đã phân kỳ từ một điểm cũ hơn.
Bởi vì cam kết trên nhánh bạn đang ở không phải là tổ tiên trực tiếp của nhánh bạn đang hợp nhất vào, Git phải làm một số việc.
Trong trường hợp này, Git thực hiện một hợp nhất ba chiều đơn giản, sử dụng hai ảnh chụp nhanh được trỏ đến bởi các đầu nhánh và tổ tiên chung của cả hai.
Thay vì chỉ di chuyển con trỏ nhánh về phía trước, Git tạo ra một ảnh chụp nhanh mới từ việc hợp nhất ba chiều này và tự động tạo ra một cam kết mới trỏ đến nó. Điều này được gọi là một cam kết hợp nhất, và đặc biệt ở chỗ nó có nhiều hơn một cha mẹ.
Bây giờ công việc của bạn đã được hợp nhất, bạn không cần nhánh iss53 nữa.
Bạn có thể đóng vấn đề trong hệ thống theo dõi vấn đề của mình và xóa nhánh:
$ git branch -d iss53
Xung đột Hợp nhất Cơ bản
Đôi khi, quá trình này không diễn ra suôn sẻ.
Nếu bạn đã thay đổi cùng một phần của cùng một tệp theo những cách khác nhau trong hai nhánh bạn đang hợp nhất, Git sẽ không thể hợp nhất chúng một cách sạch sẽ.
Nếu bản vá của bạn cho vấn đề #53 đã sửa đổi cùng một phần của một tệp như nhánh hotfix, bạn sẽ nhận được một xung đột hợp nhất trông giống như thế này:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
Git đã không tự động tạo một cam kết hợp nhất mới.
Nó đã tạm dừng quá trình trong khi bạn giải quyết xung đột.
Nếu bạn muốn xem tệp nào chưa được hợp nhất tại bất kỳ thời điểm nào sau khi xảy ra xung đột hợp nhất, bạn có thể chạy git status:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
Bất cứ thứ gì có xung đột hợp nhất và chưa được giải quyết đều được liệt kê là chưa hợp nhất. Git thêm các điểm đánh dấu giải quyết xung đột tiêu chuẩn vào các tệp có xung đột, vì vậy bạn có thể mở chúng thủ công và giải quyết các xung đột đó. Tệp của bạn chứa một phần trông giống như thế này:
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
Điều này có nghĩa là phiên bản trong HEAD (nhánh master của bạn, bởi vì đó là những gì bạn đã kiểm xuất khi bạn chạy lệnh hợp nhất của mình) là phần trên cùng của khối đó (mọi thứ phía trên =======), trong khi phiên bản trong nhánh iss53 của bạn trông giống như mọi thứ ở phần dưới cùng.
Để giải quyết xung đột, bạn phải chọn một bên hoặc bên kia hoặc tự hợp nhất nội dung.
Ví dụ: bạn có thể giải quyết xung đột này bằng cách thay thế toàn bộ khối bằng khối này:
<div id="footer">
please contact us at email.support@github.com
</div>
Giải pháp này có một chút của mỗi phần, và các dòng <<<<<<<, ======= và >>>>>>> đã bị xóa hoàn toàn.
Sau khi bạn đã giải quyết từng phần này trong mỗi tệp bị xung đột, hãy chạy git add trên mỗi tệp để đánh dấu nó là đã giải quyết.
Tổ chức tệp đánh dấu nó là đã giải quyết trong Git.
Nếu bạn muốn sử dụng một công cụ đồ họa để giải quyết các vấn đề này, bạn có thể chạy git mergetool, công cụ này sẽ kích hoạt một công cụ hợp nhất trực quan thích hợp và hướng dẫn bạn qua các xung đột:
$ git mergetool
This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html
Normal merge conflict for 'index.html':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (opendiff):
Nếu bạn muốn sử dụng một công cụ hợp nhất khác với mặc định (Git đã chọn opendiff trong trường hợp này vì lệnh được chạy trên macOS), bạn có thể thấy tất cả các công cụ được hỗ trợ được liệt kê ở trên cùng sau “one of the following tools.”
Chỉ cần nhập tên của công cụ bạn muốn sử dụng.
|
Nếu bạn cần các công cụ nâng cao hơn để giải quyết các xung đột hợp nhất phức tạp, chúng tôi sẽ đề cập thêm về việc hợp nhất trong [_advanced_merging]. |
Sau khi bạn thoát khỏi công cụ hợp nhất, Git hỏi bạn xem việc hợp nhất có thành công không.
Nếu bạn nói với tập lệnh rằng đúng như vậy, nó sẽ tổ chức tệp để đánh dấu nó là đã giải quyết cho bạn.
Bạn có thể chạy git status một lần nữa để xác minh rằng tất cả các xung đột đã được giải quyết:
$ git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: index.html
Nếu bạn hài lòng với điều đó, và bạn xác minh rằng mọi thứ có xung đột đã được tổ chức, bạn có thể nhập git commit để hoàn tất cam kết hợp nhất.
Thông điệp cam kết theo mặc định trông giống như thế này:
Merge branch 'iss53'
Conflicts:
index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
# modified: index.html
#
Nếu bạn nghĩ rằng nó sẽ hữu ích cho những người khác xem xét việc hợp nhất này trong tương lai, bạn có thể sửa đổi thông điệp cam kết này với các chi tiết về cách bạn đã giải quyết việc hợp nhất và giải thích lý do tại sao bạn đã thực hiện các thay đổi bạn đã thực hiện nếu những điều này không rõ ràng.
Quản lý Nhánh
Bây giờ bạn đã tạo, hợp nhất và xóa một số nhánh, hãy xem xét một số công cụ quản lý nhánh sẽ rất hữu ích khi bạn bắt đầu sử dụng các nhánh mọi lúc.
Lệnh git branch làm nhiều việc hơn là chỉ tạo và xóa các nhánh.
Nếu bạn chạy nó mà không có đối số, bạn sẽ nhận được một danh sách đơn giản các nhánh hiện tại của mình:
$ git branch
iss53
* master
testing
Lưu ý ký tự * đứng trước nhánh master: nó cho biết nhánh mà bạn hiện đang kiểm xuất (tức là nhánh mà HEAD trỏ đến).
Điều này có nghĩa là nếu bạn cam kết tại thời điểm này, nhánh master sẽ được di chuyển về phía trước với công việc mới của bạn.
Để xem cam kết cuối cùng trên mỗi nhánh, bạn có thể chạy git branch -v:
$ git branch -v
iss53 93b412c Fix javascript issue
* master 7a98805 Merge branch 'iss53'
testing 782fd34 Add scott to the author list in the readme
Các tùy chọn hữu ích --merged và --no-merged có thể lọc danh sách này thành các nhánh mà bạn đã hoặc chưa hợp nhất vào nhánh bạn đang ở hiện tại.
Để xem nhánh nào đã được hợp nhất vào nhánh bạn đang ở, bạn có thể chạy git branch --merged:
$ git branch --merged
iss53
* master
Bởi vì bạn đã hợp nhất iss53 trước đó, bạn thấy nó trong danh sách của mình.
Các nhánh trong danh sách này không có dấu * phía trước chúng thường là tốt để xóa bằng git branch -d; bạn đã kết hợp công việc của chúng vào một nhánh khác, vì vậy bạn sẽ không mất bất cứ thứ gì.
Để xem tất cả các nhánh chứa công việc bạn chưa hợp nhất, bạn có thể chạy git branch --no-merged:
$ git branch --no-merged
testing
Điều này hiển thị nhánh khác của bạn.
Bởi vì nó chứa công việc chưa được hợp nhất, việc cố gắng xóa nó bằng git branch -d sẽ thất bại:
$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.
Nếu bạn thực sự muốn xóa nhánh và mất công việc đó, bạn có thể buộc nó bằng -D, như thông báo hữu ích chỉ ra.
|
Các tùy chọn được mô tả ở trên, Bạn luôn có thể cung cấp thêm một đối số để hỏi về trạng thái hợp nhất đối với một số nhánh khác mà không cần kiểm xuất nhánh khác đó trước, như trong, cái gì chưa được hợp nhất vào nhánh
|
Thay đổi tên nhánh
|
Không đổi tên các nhánh vẫn đang được sử dụng bởi các cộng tác viên khác. Không đổi tên một nhánh như master/main/mainline mà không đọc phần Thay đổi tên nhánh master. |
Giả sử bạn có một nhánh được gọi là bad-branch-name và bạn muốn đổi tên nó thành corrected-branch-name, trong khi vẫn giữ tất cả lịch sử.
Bạn cũng muốn thay đổi tên nhánh trên remote (GitHub, GitLab, máy chủ khác).
Làm thế nào để bạn làm điều này?
Đổi tên nhánh cục bộ bằng lệnh git branch --move:
$ git branch --move bad-branch-name corrected-branch-name
Điều này thay thế bad-branch-name của bạn bằng corrected-branch-name, nhưng thay đổi này chỉ là cục bộ vào lúc này.
Để cho người khác thấy nhánh đã sửa trên remote, hãy đẩy nó:
$ git push --set-upstream origin corrected-branch-name
Bây giờ chúng ta sẽ xem xét ngắn gọn về nơi chúng ta đang ở hiện tại:
$ git branch --all
* corrected-branch-name
main
remotes/origin/bad-branch-name
remotes/origin/corrected-branch-name
remotes/origin/main
Lưu ý rằng bạn đang ở trên nhánh corrected-branch-name và nó có sẵn trên remote.
Tuy nhiên, nhánh có tên xấu cũng vẫn hiện diện ở đó nhưng bạn có thể xóa nó bằng cách thực hiện lệnh sau:
$ git push origin --delete bad-branch-name
Bây giờ tên nhánh xấu đã được thay thế hoàn toàn bằng tên nhánh đã sửa.
Thay đổi tên nhánh master
|
Thay đổi tên của một nhánh như master/main/mainline/default sẽ phá vỡ các tích hợp, dịch vụ, tiện ích trợ giúp và tập lệnh xây dựng/phát hành mà kho lưu trữ của bạn sử dụng. Trước khi bạn làm điều này, hãy chắc chắn rằng bạn tham khảo ý kiến với các cộng tác viên của mình. Ngoài ra, hãy chắc chắn rằng bạn thực hiện tìm kiếm kỹ lưỡng qua repo của mình và cập nhật bất kỳ tham chiếu nào đến tên nhánh cũ trong mã và tập lệnh của bạn. |
Đổi tên nhánh master cục bộ của bạn thành main bằng lệnh sau:
$ git branch --move master main
Không còn nhánh master cục bộ nữa, vì nó đã được đổi tên thành nhánh main.
Để cho người khác thấy nhánh main mới, bạn cần đẩy nó lên remote.
Điều này làm cho nhánh đã đổi tên có sẵn trên remote.
$ git push --set-upstream origin main
Bây giờ chúng ta kết thúc với trạng thái sau:
$ git branch --all
* main
remotes/origin/HEAD -> origin/master
remotes/origin/main
remotes/origin/master
Nhánh master cục bộ của bạn đã biến mất, vì nó được thay thế bằng nhánh main.
Nhánh main hiện diện trên remote.
Tuy nhiên, nhánh master cũ vẫn hiện diện trên remote.
Các cộng tác viên khác sẽ tiếp tục sử dụng nhánh master làm cơ sở cho công việc của họ, cho đến khi bạn thực hiện thêm một số thay đổi.
Bây giờ bạn có thêm một vài nhiệm vụ trước mắt để hoàn thành quá trình chuyển đổi:
-
Bất kỳ dự án nào phụ thuộc vào dự án này sẽ cần cập nhật mã và/hoặc cấu hình của chúng.
-
Cập nhật bất kỳ tệp cấu hình trình chạy thử nghiệm nào.
-
Điều chỉnh các tập lệnh xây dựng và phát hành.
-
Chuyển hướng cài đặt trên máy chủ lưu trữ repo của bạn cho những thứ như nhánh mặc định của repo, quy tắc hợp nhất và những thứ khác khớp với tên nhánh.
-
Cập nhật tham chiếu đến nhánh cũ trong tài liệu.
-
Đóng hoặc hợp nhất bất kỳ yêu cầu kéo nào nhắm mục tiêu đến nhánh cũ.
Sau khi bạn đã thực hiện tất cả các tác vụ này và chắc chắn rằng nhánh main hoạt động giống như nhánh master, bạn có thể xóa nhánh master:
$ git push origin --delete master
Quy trình làm việc Phân nhánh
Bây giờ bạn đã nắm được những điều cơ bản về phân nhánh và hợp nhất, bạn có thể hoặc nên làm gì với chúng? Trong phần này, chúng ta sẽ đề cập đến một số quy trình làm việc phổ biến mà việc phân nhánh nhẹ này thực hiện được, vì vậy bạn có thể quyết định xem bạn có muốn kết hợp chúng vào chu trình phát triển của riêng mình hay không.
Các Nhánh Chạy Dài
Bởi vì Git sử dụng hợp nhất ba chiều đơn giản, việc hợp nhất từ nhánh này sang nhánh khác nhiều lần trong một thời gian dài thường dễ thực hiện. Điều này có nghĩa là bạn có thể có một vài nhánh luôn mở và bạn sử dụng cho các giai đoạn khác nhau của chu trình phát triển của mình; bạn có thể hợp nhất thường xuyên từ một số trong số chúng vào những cái khác.
Nhiều nhà phát triển Git có một quy trình làm việc bao gồm cách tiếp cận này, chẳng hạn như chỉ có mã hoàn toàn ổn định trong nhánh master của họ — có thể chỉ là mã đã hoặc sẽ được phát hành.
Họ có một nhánh song song khác tên là develop hoặc next mà họ làm việc hoặc sử dụng để kiểm tra độ ổn định — nó không nhất thiết phải luôn ổn định, nhưng bất cứ khi nào nó đạt đến trạng thái ổn định, nó có thể được hợp nhất vào master.
Nó được sử dụng để kéo các nhánh chủ đề (các nhánh tồn tại trong thời gian ngắn, giống như nhánh iss53 trước đó của bạn) khi chúng đã sẵn sàng, để đảm bảo chúng vượt qua tất cả các bài kiểm tra và không gây ra lỗi.
Trong thực tế, chúng ta đang nói về các con trỏ di chuyển lên dòng cam kết mà bạn đang thực hiện. Các nhánh ổn định ở xa hơn dòng trong lịch sử cam kết của bạn, và các nhánh tiên tiến (bleeding-edge) ở xa hơn lịch sử.
Nói chung, dễ dàng hơn để nghĩ về chúng như các silo công việc, nơi các bộ cam kết tốt nghiệp sang một silo ổn định hơn khi chúng được kiểm tra đầy đủ.
Bạn có thể tiếp tục làm điều này cho một số cấp độ ổn định.
Một số dự án lớn hơn cũng có một nhánh proposed hoặc pu (cập nhật đề xuất - proposed updates) có các nhánh tích hợp có thể chưa sẵn sàng để đi vào nhánh next hoặc master.
Ý tưởng là các nhánh của bạn ở các mức độ ổn định khác nhau; khi chúng đạt đến mức ổn định hơn, chúng được hợp nhất vào nhánh phía trên chúng.
Một lần nữa, việc có nhiều nhánh chạy dài là không cần thiết, nhưng nó thường hữu ích, đặc biệt là khi bạn đang xử lý các dự án rất lớn hoặc phức tạp.
Các Nhánh Chủ đề
Tuy nhiên, các nhánh chủ đề rất hữu ích trong các dự án ở mọi quy mô. Một nhánh chủ đề là một nhánh tồn tại trong thời gian ngắn mà bạn tạo và sử dụng cho một tính năng cụ thể hoặc công việc liên quan. Đây là điều mà bạn có thể chưa bao giờ làm với VCS trước đây vì việc tạo và hợp nhất các nhánh thường quá tốn kém. Nhưng trong Git, việc tạo, làm việc, hợp nhất và xóa các nhánh nhiều lần trong ngày là điều phổ biến.
Bạn đã thấy điều này trong phần trước với các nhánh iss53 và hotfix mà bạn đã tạo.
Bạn đã thực hiện một vài cam kết trên chúng và xóa chúng trực tiếp sau khi hợp nhất chúng vào nhánh chính của mình.
Kỹ thuật này cho phép bạn chuyển đổi ngữ cảnh nhanh chóng và hoàn toàn — bởi vì công việc của bạn được tách thành các silo nơi tất cả các thay đổi trong nhánh đó đều liên quan đến chủ đề đó, nên dễ dàng hơn để xem những gì đã xảy ra trong quá trình xem xét mã và những thứ tương tự.
Bạn có thể giữ các thay đổi ở đó trong vài phút, vài ngày hoặc vài tháng và hợp nhất chúng khi chúng sẵn sàng, bất kể thứ tự chúng được tạo hoặc làm việc.
Hãy xem xét một ví dụ về việc thực hiện một số công việc (trên master), phân nhánh cho một vấn đề (iss91), làm việc trên đó một chút, phân nhánh khỏi nhánh thứ hai để thử một cách khác để xử lý cùng một vấn đề (iss91v2), quay lại nhánh master của bạn và làm việc ở đó một lúc, và sau đó phân nhánh ở đó để thực hiện một số công việc mà bạn không chắc là một ý tưởng hay (nhánh dumbidea).
Lịch sử cam kết của bạn sẽ trông giống như thế này:
Bây giờ, giả sử bạn quyết định bạn thích giải pháp thứ hai cho vấn đề của mình nhất (iss91v2); và bạn đã cho đồng nghiệp xem nhánh dumbidea, và nó trở thành thiên tài.
Bạn có thể vứt bỏ nhánh iss91 ban đầu (mất các cam kết C5 và C6) và hợp nhất hai nhánh kia.
Lịch sử của bạn sau đó trông giống như thế này:
dumbidea và iss91v2Chúng tôi sẽ đi sâu hơn vào chi tiết về các quy trình làm việc khác nhau có thể có cho dự án Git của bạn trong Git phân tán, vì vậy trước khi bạn quyết định sơ đồ phân nhánh nào dự án tiếp theo của bạn sẽ sử dụng, hãy chắc chắn đọc chương đó.
Điều quan trọng cần nhớ khi bạn đang làm tất cả những điều này là các nhánh này hoàn toàn là cục bộ. Khi bạn đang phân nhánh và hợp nhất, mọi thứ chỉ được thực hiện trong kho lưu trữ Git của bạn — không có giao tiếp với máy chủ.
Các Nhánh Từ xa
Các tham chiếu từ xa (remote references) là các tham chiếu (con trỏ) trong kho lưu trữ từ xa của bạn, bao gồm các nhánh, thẻ, v.v.
Bạn có thể nhận được danh sách đầy đủ các tham chiếu từ xa một cách rõ ràng bằng git ls-remote <remote>, hoặc git remote show <remote> cho các nhánh từ xa cũng như nhiều thông tin khác.
Tuy nhiên, một cách phổ biến hơn là tận dụng các nhánh theo dõi từ xa (remote-tracking branches).
Các nhánh theo dõi từ xa là các tham chiếu đến trạng thái của các nhánh từ xa. Chúng là các tham chiếu cục bộ mà bạn không thể di chuyển; Git di chuyển chúng cho bạn bất cứ khi nào bạn thực hiện bất kỳ giao tiếp mạng nào, để đảm bảo chúng thể hiện chính xác trạng thái của kho lưu trữ từ xa. Hãy nghĩ về chúng như những dấu trang (bookmarks) để nhắc nhở bạn về vị trí của các nhánh trên kho lưu trữ từ xa của bạn vào lần cuối cùng bạn kết nối với chúng.
Tên nhánh theo dõi từ xa có dạng <remote>/<branch>.
Ví dụ: nếu bạn muốn xem nhánh master trên remote origin của mình trông như thế nào vào lần cuối cùng bạn giao tiếp với nó, bạn sẽ kiểm tra nhánh origin/master.
Nếu bạn đang làm việc trên một vấn đề với một đối tác và họ đã đẩy lên một nhánh iss53, bạn có thể có nhánh iss53 cục bộ của riêng mình; nhưng nhánh trên máy chủ sẽ được đại diện bởi nhánh theo dõi từ xa origin/iss53.
Điều này có thể hơi khó hiểu, vì vậy hãy xem xét một ví dụ.
Giả sử bạn có một máy chủ Git trên mạng của mình tại git.ourcompany.com.
Nếu bạn sao chép từ đây, lệnh git clone của Git sẽ tự động đặt tên cho nó là origin cho bạn, kéo xuống tất cả dữ liệu của nó, tạo một con trỏ đến nơi nhánh master của nó đang ở và đặt tên cục bộ là origin/master.
Git cũng cung cấp cho bạn nhánh master cục bộ của riêng bạn bắt đầu từ cùng một nơi với nhánh master của origin, vì vậy bạn có một cái gì đó để làm việc.
|
“origin” không đặc biệt
Cũng giống như tên nhánh “master” không có ý nghĩa đặc biệt nào trong Git, “origin” cũng không có ý nghĩa đặc biệt nào.
|
Nếu bạn thực hiện một số công việc trên nhánh master cục bộ của mình, và trong khi đó, ai đó khác đẩy lên git.ourcompany.com và cập nhật nhánh master của nó, thì lịch sử của bạn sẽ di chuyển về phía trước theo cách khác.
Ngoài ra, miễn là bạn không liên lạc với máy chủ origin của mình, con trỏ origin/master của bạn sẽ không di chuyển.
Để đồng bộ hóa công việc của bạn với một remote nhất định, bạn chạy lệnh git fetch <remote> (trong trường hợp của chúng ta là git fetch origin).
Lệnh này tra cứu máy chủ nào là “origin” (trong trường hợp này là git.ourcompany.com), lấy bất kỳ dữ liệu nào từ nó mà bạn chưa có, và cập nhật cơ sở dữ liệu cục bộ của bạn, di chuyển con trỏ origin/master của bạn đến vị trí mới, cập nhật hơn của nó.
Để chứng minh việc có nhiều máy chủ từ xa và các nhánh từ xa cho các dự án từ xa đó trông như thế nào, hãy giả sử bạn có một máy chủ Git nội bộ khác chỉ được sử dụng để phát triển bởi một trong các nhóm chạy nước rút (sprint) của bạn.
Máy chủ này tại git.team1.ourcompany.com.
Bạn có thể thêm nó làm tham chiếu từ xa mới cho dự án bạn đang làm việc bằng cách chạy lệnh git remote add như chúng ta đã đề cập trong Các khái niệm cơ bản về Git.
Đặt tên cho remote này là teamone, đây sẽ là tên viết tắt của bạn cho toàn bộ URL đó.
Bây giờ, bạn có thể chạy git fetch teamone để lấy mọi thứ mà máy chủ teamone có mà bạn chưa có.
Bởi vì máy chủ đó có một tập hợp con dữ liệu mà máy chủ origin của bạn có ngay bây giờ, Git không lấy bất kỳ dữ liệu nào nhưng đặt một nhánh theo dõi từ xa có tên teamone/master để trỏ đến cam kết mà teamone có làm nhánh master của nó.
Đẩy (Pushing)
Khi bạn muốn chia sẻ một nhánh với mọi người, bạn cần đẩy nó lên một remote mà bạn có quyền ghi. Các nhánh cục bộ của bạn không được tự động đồng bộ hóa với các remote bạn ghi vào — bạn phải đẩy rõ ràng các nhánh bạn muốn chia sẻ. Bằng cách đó, bạn có thể sử dụng các nhánh riêng tư cho công việc bạn không muốn chia sẻ và chỉ đẩy lên các nhánh chủ đề mà bạn muốn cộng tác.
Nếu bạn có một nhánh tên là serverfix mà bạn muốn làm việc với những người khác, bạn có thể đẩy nó lên giống như cách bạn đã đẩy nhánh đầu tiên của mình.
Chạy git push <remote> <branch>:
$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
* [new branch] serverfix -> serverfix
Đây là một chút đường tắt.
Git tự động mở rộng tên nhánh serverfix thành refs/heads/serverfix:refs/heads/serverfix, có nghĩa là, "Lấy nhánh serverfix cục bộ của tôi và đẩy nó để cập nhật nhánh serverfix từ xa."
Chúng ta sẽ xem xét phần refs/heads/ chi tiết trong [ch10-git-internals], nhưng bạn thường có thể bỏ qua nó.
Bạn cũng có thể thực hiện git push origin serverfix:serverfix, làm điều tương tự — nó nói, "Lấy serverfix của tôi và biến nó thành serverfix của remote."
Bạn có thể sử dụng định dạng này để đẩy một nhánh cục bộ vào một nhánh từ xa có tên khác.
Nếu bạn không muốn nó được gọi là serverfix trên remote, thay vào đó bạn có thể chạy git push origin serverfix:awesomebranch để đẩy nhánh serverfix cục bộ của bạn lên nhánh awesomebranch trên dự án từ xa.
|
Đừng nhập mật khẩu của bạn mỗi lần.
Nếu bạn đang sử dụng URL HTTPS để đẩy qua, máy chủ Git sẽ hỏi bạn tên người dùng và mật khẩu để xác thực. Theo mặc định, nó sẽ nhắc bạn trên thiết bị đầu cuối để nhập thông tin này để máy chủ có thể xác định xem bạn có được phép đẩy hay không. Nếu bạn không muốn nhập nó mỗi lần bạn đẩy, bạn có thể thiết lập "bộ nhớ đệm thông tin xác thực" (credential cache).
Cách đơn giản nhất là chỉ cần giữ nó trong bộ nhớ trong vài phút, bạn có thể dễ dàng thiết lập bằng cách chạy Để biết thêm thông tin về các tùy chọn bộ nhớ đệm thông tin xác thực khác nhau có sẵn, xem [_credential_caching]. |
Lần tới khi một trong những cộng tác viên của bạn lấy từ máy chủ, họ sẽ nhận được tham chiếu đến nơi phiên bản serverfix của máy chủ nằm dưới nhánh theo dõi từ xa origin/serverfix:
$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
* [new branch] serverfix -> origin/serverfix
Điều quan trọng cần lưu ý là khi bạn thực hiện tìm nạp (fetch) mang xuống các nhánh theo dõi từ xa mới, bạn không tự động có các bản sao cục bộ, có thể chỉnh sửa của chúng.
Nói cách khác, trong trường hợp này, bạn không có nhánh serverfix mới — bạn chỉ có một con trỏ origin/serverfix mà bạn không thể sửa đổi.
Để hợp nhất công việc này vào nhánh làm việc hiện tại của bạn, bạn có thể chạy git merge origin/serverfix.
Nếu bạn muốn có nhánh serverfix của riêng mình để bạn có thể làm việc trên đó, bạn có thể dựa trên nhánh theo dõi từ xa của mình:
$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
Điều này cung cấp cho bạn một nhánh cục bộ mà bạn có thể làm việc trên đó bắt đầu từ nơi origin/serverfix đang ở.
Các Nhánh Theo dõi
Kiểm xuất một nhánh cục bộ từ một nhánh theo dõi từ xa sẽ tự động tạo ra cái được gọi là "nhánh theo dõi" (tracking branch) (và nhánh nó theo dõi được gọi là "nhánh thượng nguồn" - upstream branch).
Các nhánh theo dõi là các nhánh cục bộ có mối quan hệ trực tiếp với một nhánh từ xa.
Nếu bạn đang ở trên một nhánh theo dõi và gõ git pull, Git sẽ tự động biết máy chủ nào để lấy và nhánh nào để hợp nhất vào.
Khi bạn sao chép một kho lưu trữ, nó thường tự động tạo một nhánh master theo dõi origin/master.
Tuy nhiên, bạn có thể thiết lập các nhánh theo dõi khác nếu bạn muốn — những nhánh theo dõi các nhánh trên các remote khác hoặc không theo dõi nhánh master.
Trường hợp đơn giản là ví dụ bạn vừa thấy, chạy git checkout -b <branch> <remote>/<branch>.
Đây là một thao tác đủ phổ biến đến nỗi Git cung cấp cờ tắt --track:
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
Trên thực tế, điều này phổ biến đến mức thậm chí còn có một lối tắt cho lối tắt đó. Nếu tên nhánh bạn đang cố gắng kiểm xuất (a) không tồn tại và (b) khớp chính xác với tên trên chỉ một remote, Git sẽ tạo một nhánh theo dõi cho bạn:
$ git checkout serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
Để thiết lập một nhánh cục bộ với tên khác với nhánh từ xa, bạn có thể dễ dàng sử dụng phiên bản đầu tiên với tên nhánh cục bộ khác:
$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'
Bây giờ, nhánh cục bộ sf của bạn sẽ tự động kéo từ origin/serverfix.
Nếu bạn đã có một nhánh cục bộ và muốn đặt nó thành một nhánh từ xa bạn vừa kéo xuống, hoặc muốn thay đổi nhánh thượng nguồn bạn đang theo dõi, bạn có thể sử dụng tùy chọn -u hoặc --set-upstream-to cho git branch để đặt rõ ràng nó bất cứ lúc nào.
$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
|
Thượng nguồn viết tắt
Khi bạn đã thiết lập nhánh theo dõi, bạn có thể tham chiếu nhánh thượng nguồn của nó bằng cách viết tắt |
Nếu bạn muốn xem những nhánh theo dõi nào bạn đã thiết lập, bạn có thể sử dụng tùy chọn -vv cho git branch.
Điều này sẽ liệt kê các nhánh cục bộ của bạn với nhiều thông tin hơn bao gồm những gì mỗi nhánh đang theo dõi và liệu nhánh cục bộ của bạn đang đi trước, đi sau hay cả hai.
$ git branch -vv
iss53 7e424c3 [origin/iss53: ahead 2] forgot the brackets
master 1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
testing 5ea463a trying something new
Vì vậy, ở đây chúng ta có thể thấy rằng nhánh iss53 của chúng ta đang theo dõi origin/iss53 và đang "đi trước" (ahead) hai, nghĩa là chúng ta có hai cam kết cục bộ chưa được đẩy lên máy chủ.
Chúng ta cũng có thể thấy rằng nhánh master của chúng ta đang theo dõi origin/master và được cập nhật.
Tiếp theo, chúng ta có thể thấy rằng nhánh serverfix của chúng ta đang theo dõi nhánh server-fix-good trên máy chủ teamone của chúng ta và đi trước ba và đi sau một, nghĩa là có một cam kết trên máy chủ mà chúng ta chưa hợp nhất và ba cam kết cục bộ mà chúng ta chưa đẩy.
Cuối cùng, nhánh testing của chúng ta không theo dõi bất kỳ nhánh từ xa nào.
Điều quan trọng cần lưu ý là những con số này chỉ nói về lần cuối cùng bạn lấy từ mỗi máy chủ. Lệnh này không tiếp cận các máy chủ, nó cho bạn biết về những gì nó đã lưu trữ từ các máy chủ này lần cuối cùng bạn nói chuyện với chúng. Nếu bạn muốn hoàn toàn cập nhật các số đi trước và đi sau này, bạn sẽ cần lấy từ tất cả các remote của mình ngay trước khi chạy cái này. Bạn có thể làm điều đó như thế này:
$ git fetch --all; git branch -vv
Kéo (Pulling)
Trong khi lệnh git fetch sẽ lấy xuống tất cả các thay đổi trên máy chủ mà bạn chưa có, nó sẽ không sửa đổi thư mục làm việc của bạn chút nào.
Nó sẽ chỉ lấy dữ liệu và để bạn tự hợp nhất nó.
Tuy nhiên, có một lệnh gọi là git pull về cơ bản là git fetch ngay lập tức theo sau là git merge trong hầu hết các trường hợp.
Nếu bạn có một nhánh theo dõi được thiết lập như đã trình bày trong phần trước, bằng cách thiết lập rõ ràng hoặc bằng cách tạo nó cho bạn bằng các lệnh clone hoặc checkout, git pull sẽ tra cứu máy chủ và nhánh nào nhánh hiện tại của bạn đang theo dõi, tìm nạp từ máy chủ đó và sau đó cố gắng hợp nhất nhánh từ xa đó vào.
Nói chung, tốt hơn là chỉ cần sử dụng các lệnh fetch và merge một cách rõ ràng vì sự kỳ diệu của git pull thường có thể gây nhầm lẫn.
Xóa Nhánh Từ xa
Giả sử bạn đã hoàn thành một nhánh từ xa — giả sử bạn và các cộng tác viên của bạn đã hoàn thành một tính năng và đã hợp nhất nó vào nhánh master của remote của bạn (hoặc bất kỳ nhánh nào mà dòng mã ổn định của bạn đang ở).
Bạn có thể xóa một nhánh từ xa bằng tùy chọn --delete cho git push.
Nếu bạn muốn xóa nhánh serverfix của mình khỏi máy chủ, bạn chạy như sau:
$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
- [deleted] serverfix
Về cơ bản, tất cả những gì làm là xóa con trỏ khỏi máy chủ. Máy chủ Git thường giữ dữ liệu ở đó trong một thời gian cho đến khi bộ thu gom rác chạy, vì vậy nếu nó vô tình bị xóa, thường dễ dàng khôi phục.
Rebasing
Trong Git, có hai cách chính để tích hợp các thay đổi từ nhánh này sang nhánh khác: merge và rebase.
Trong phần này, bạn sẽ tìm hiểu rebase là gì, cách thực hiện, tại sao nó là một công cụ tuyệt vời và trong trường hợp nào bạn không nên sử dụng nó.
Rebase Cơ bản
Nếu bạn quay lại ví dụ trước đó từ Hợp nhất Cơ bản, bạn có thể thấy rằng bạn đã phân kỳ công việc của mình và thực hiện các cam kết trên hai nhánh khác nhau.
Cách dễ nhất để tích hợp các nhánh, như chúng tôi đã đề cập, là lệnh merge.
Nó thực hiện hợp nhất ba chiều giữa hai ảnh chụp nhanh mới nhất của nhánh (C3 và C4) và tổ tiên chung mới nhất của cả hai (C2), tạo ra một ảnh chụp nhanh mới (và cam kết).
Tuy nhiên, có một cách khác: bạn có thể lấy bản vá của thay đổi đã được giới thiệu trong C4 và áp dụng lại nó trên C3.
Trong Git, điều này được gọi là rebasing.
Với lệnh rebase, bạn có thể lấy tất cả các thay đổi đã được cam kết trên một nhánh và phát lại chúng trên một nhánh khác.
Trong ví dụ này, bạn sẽ kiểm xuất nhánh experiment, và sau đó rebase nó lên nhánh master như sau:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
Thao tác này hoạt động bằng cách đi tới tổ tiên chung của hai nhánh (nhánh bạn đang ở và nhánh bạn đang rebase), lấy sự khác biệt (diff) được giới thiệu bởi mỗi cam kết của nhánh bạn đang ở, lưu các khác biệt đó vào các tệp tạm thời, đặt lại nhánh hiện tại về cùng một cam kết với nhánh bạn đang rebase, và cuối cùng áp dụng từng thay đổi lần lượt.
C4 lên C3Tại thời điểm này, bạn có thể quay lại nhánh master và thực hiện hợp nhất tua nhanh (fast-forward merge).
$ git checkout master
$ git merge experiment
masterBây giờ, ảnh chụp nhanh được trỏ tới bởi C4' hoàn toàn giống với ảnh chụp nhanh được trỏ tới bởi C5 trong ví dụ hợp nhất.
Không có sự khác biệt nào trong sản phẩm cuối cùng của quá trình tích hợp, nhưng rebasing tạo ra một lịch sử sạch hơn.
Nếu bạn kiểm tra nhật ký của một nhánh đã được rebase, nó trông giống như một lịch sử tuyến tính: có vẻ như tất cả công việc đã xảy ra theo chuỗi, ngay cả khi nó thực sự xảy ra song song.
Thông thường, bạn sẽ làm điều này để đảm bảo các cam kết của bạn áp dụng sạch sẽ trên một nhánh từ xa — có lẽ trong một dự án mà bạn đang cố gắng đóng góp nhưng bạn không duy trì.
Trong trường hợp này, bạn sẽ làm công việc của mình trong một nhánh và sau đó rebase công việc của bạn lên origin/master khi bạn sẵn sàng gửi các bản vá của mình cho dự án chính.
Bằng cách đó, người bảo trì không phải thực hiện bất kỳ công việc tích hợp nào — chỉ cần tua nhanh hoặc áp dụng sạch sẽ.
Lưu ý rằng ảnh chụp nhanh được trỏ tới bởi cam kết cuối cùng bạn nhận được, cho dù đó là cam kết cuối cùng của các cam kết được rebase hay cam kết hợp nhất cuối cùng sau khi hợp nhất, đều giống nhau — chỉ có lịch sử là khác nhau. Rebasing phát lại các thay đổi từ dòng công việc này sang dòng công việc khác theo thứ tự chúng được giới thiệu, trong khi hợp nhất lấy các điểm cuối và hợp nhất chúng lại với nhau.
Các Rebase Thú vị hơn
Bạn cũng có thể rebase của mình lên một cái gì đó khác ngoài nhánh mục tiêu rebase.
Lấy ví dụ lịch sử như Lịch sử với một nhánh chủ đề tách ra từ một nhánh chủ đề khác, chẳng hạn.
Bạn đã tạo một nhánh chủ đề (server) để thêm một số chức năng phía máy chủ vào dự án của mình và thực hiện một cam kết.
Sau đó, bạn phân nhánh đó để thực hiện thay đổi phía máy khách (client) và cam kết một vài lần.
Cuối cùng, bạn quay lại nhánh máy chủ của mình và thực hiện thêm một vài cam kết.
Giả sử bạn quyết định rằng bạn muốn hợp nhất các thay đổi phía máy khách của mình vào dòng chính để phát hành, nhưng bạn muốn giữ lại các thay đổi phía máy chủ cho đến khi nó được kiểm tra thêm.
Bạn có thể lấy các thay đổi trên client không có trên server (C8 và C9) và phát lại chúng trên nhánh master của bạn bằng cách sử dụng tùy chọn --onto của git rebase:
$ git rebase --onto master server client
Về cơ bản, điều này nói, “Kiểm xuất nhánh client, tìm ra các bản vá từ tổ tiên chung của nhánh client và server, và sau đó phát lại chúng trên master.”
Nó hơi phức tạp, nhưng kết quả thật tuyệt vời.
Bây giờ bạn có thể tua nhanh nhánh master của mình (xem Tua nhanh nhánh master của bạn để bao gồm các thay đổi của nhánh client):
$ git checkout master
$ git merge client
master của bạn để bao gồm các thay đổi của nhánh clientGiả sử bạn quyết định kéo cả nhánh máy chủ của mình vào.
Bạn có thể rebase nhánh server lên nhánh master mà không cần phải kiểm xuất nó trước bằng cách chạy git rebase <basebranch> <topicbranch> — lệnh này kiểm xuất nhánh chủ đề (trong trường hợp này là server) cho bạn và phát lại nó lên nhánh cơ sở (base branch) (master):
$ git rebase master server
Điều này phát lại công việc server của bạn lên trên công việc master của bạn, như được hiển thị trong Rebasing nhánh server của bạn lên trên nhánh master của bạn.
Sau đó, bạn có thể tua nhanh nhánh cơ sở (master):
$ git checkout master
$ git merge server
Bạn có thể xóa các nhánh client và server vì tất cả công việc đã được tích hợp và bạn không cần chúng nữa, để lại lịch sử của bạn cho toàn bộ quá trình này trông giống như Lịch sử cam kết cuối cùng:
$ git branch -d client
$ git branch -d server
Sự Nguy hiểm của Rebasing
Aah, nhưng niềm vui của rebasing không phải là không có nhược điểm của nó, có thể được tóm tắt trong một dòng duy nhất:
Đừng rebase các cam kết tồn tại bên ngoài kho lưu trữ của bạn và những người khác có thể đã dựa trên công việc đó.
Nếu bạn tuân theo hướng dẫn đó, bạn sẽ ổn. Nếu bạn không, mọi người sẽ ghét bạn, và bạn sẽ bị bạn bè và gia đình khinh miệt.
Khi bạn rebase nội dung, bạn đang từ bỏ các cam kết hiện có và tạo ra các cam kết mới tương tự nhưng khác biệt.
Nếu bạn đẩy các cam kết đi đâu đó và những người khác kéo chúng xuống và dựa vào công việc trên đó, và sau đó bạn viết lại các cam kết đó bằng git rebase và đẩy chúng lên lại, các cộng tác viên của bạn sẽ phải hợp nhất lại công việc của họ và mọi thứ sẽ trở nên lộn xộn khi bạn cố gắng kéo công việc của họ trở lại vào của bạn.
Hãy xem xét một ví dụ về cách việc rebase công việc mà bạn đã công khai có thể gây ra vấn đề. Giả sử bạn sao chép từ một máy chủ trung tâm và sau đó thực hiện một số công việc trên đó. Lịch sử cam kết của bạn trông giống như thế này:
Bây giờ, ai đó khác thực hiện nhiều công việc hơn bao gồm một hợp nhất, và đẩy công việc đó lên máy chủ trung tâm. Bạn lấy nó và hợp nhất nhánh từ xa mới vào công việc của mình, làm cho lịch sử của bạn trông giống như thế này:
Tiếp theo, người đã đẩy công việc đã hợp nhất quyết định quay lại và rebase công việc của họ thay thế; họ thực hiện git push --force để ghi đè lên lịch sử trên máy chủ.
Sau đó, bạn lấy từ máy chủ đó, mang xuống các cam kết mới.
Bây giờ cả hai bạn đang ở trong tình thế khó khăn.
Nếu bạn thực hiện git pull, bạn sẽ tạo ra một cam kết hợp nhất bao gồm cả hai dòng lịch sử, và kho lưu trữ của bạn sẽ trông giống như thế này:
Nếu bạn chạy git log khi lịch sử của bạn trông giống như thế này, bạn sẽ thấy hai cam kết có cùng tác giả, ngày tháng và thông điệp, điều này sẽ gây nhầm lẫn.
Hơn nữa, nếu bạn đẩy lịch sử này trở lại máy chủ, bạn sẽ giới thiệu lại tất cả các cam kết đã được rebase đó vào máy chủ trung tâm, điều này có thể gây nhầm lẫn thêm cho mọi người.
Khá an toàn khi cho rằng nhà phát triển khác không muốn C4 và C6 có trong lịch sử; đó là lý do tại sao họ rebase ngay từ đầu.
Rebase Khi Bạn Rebase
Nếu bạn thấy mình trong tình huống như thế này, Git có thêm một số phép thuật có thể giúp bạn. Nếu ai đó trong nhóm của bạn buộc đẩy các thay đổi ghi đè lên công việc mà bạn đã dựa trên đó, thử thách của bạn là tìm ra cái gì là của bạn và cái gì họ đã viết lại.
Hóa ra là ngoài tổng kiểm tra SHA-1 cam kết, Git cũng tính toán một tổng kiểm tra chỉ dựa trên bản vá được giới thiệu với cam kết. Đây được gọi là "patch-id".
Nếu bạn kéo xuống công việc đã được viết lại và rebase nó lên trên các cam kết mới từ đối tác của bạn, Git thường có thể tìm ra thành công những gì là duy nhất của bạn và áp dụng lại chúng lên trên nhánh mới.
Ví dụ, trong kịch bản trước, nếu thay vì thực hiện hợp nhất khi chúng ta ở Ai đó đẩy các cam kết đã rebase, từ bỏ các cam kết bạn đã dựa trên công việc của mình, chúng ta chạy git rebase teamone/master, Git sẽ:
-
Xác định công việc nào là duy nhất cho nhánh của chúng ta (
C2,C3,C4,C6,C7) -
Xác định cái nào không phải là cam kết hợp nhất (
C2,C3,C4) -
Xác định cái nào chưa được viết lại vào nhánh mục tiêu (chỉ
C2vàC3, vìC4giống vớiC4') -
Áp dụng các cam kết đó vào đầu
teamone/master
Vì vậy, thay vì kết quả chúng ta thấy trong Bạn hợp nhất trong cùng một công việc một lần nữa vào một cam kết hợp nhất mới, chúng ta sẽ nhận được một cái gì đó giống như Rebase lên trên công việc bị buộc đẩy (force-pushed).
Điều này chỉ hoạt động nếu C4 và C4' mà đối tác của bạn đã thực hiện gần như giống hệt nhau về bản vá.
Nếu không, rebase sẽ không thể biết rằng nó là một bản sao và sẽ thêm một cam kết giống C4 khác (có thể sẽ thất bại khi áp dụng sạch sẽ, vì các thay đổi ít nhất đã có ở đó).
Bạn cũng có thể đơn giản hóa điều này bằng cách chạy git pull --rebase thay vì git pull bình thường.
Hoặc bạn có thể làm điều đó thủ công với git fetch theo sau là git rebase teamone/master trong trường hợp này.
Nếu bạn đang sử dụng git pull và muốn đặt --rebase làm mặc định, bạn có thể đặt giá trị cấu hình pull.rebase bằng một cái gì đó như git config --global pull.rebase true.
Nếu bạn chỉ bao giờ rebase các cam kết chưa bao giờ rời khỏi máy tính của riêng bạn, bạn sẽ ổn. Nếu bạn rebase các cam kết đã được đẩy, nhưng không ai khác đã dựa trên các cam kết đó, bạn cũng sẽ ổn. Nếu bạn rebase các cam kết đã được đẩy công khai, và mọi người có thể đã dựa trên công việc của họ, thì bạn có thể gặp rắc rối bực bội, và sự khinh miệt của đồng đội.
Nếu bạn hoặc đối tác của bạn thấy cần thiết tại một thời điểm nào đó, hãy đảm bảo mọi người chạy git pull --rebase để cố gắng làm cho nỗi đau bớt đi một chút.
Rebase so với Merge
Bây giờ bạn đã thấy rebasing và merging hoạt động, bạn có thể tự hỏi cái nào tốt hơn. Trước khi chúng ta có thể trả lời điều này, hãy lùi lại một chút và nói về lịch sử có ý nghĩa gì.
Một quan điểm cho rằng lịch sử kho lưu trữ của bạn là ghi chép về những gì đã thực sự xảy ra. Đó là một tài liệu lịch sử, có giá trị theo đúng nghĩa của nó, và không nên bị giả mạo. Theo quan điểm này, thay đổi lịch sử cam kết là báng bổ; bạn đang nói dối về những gì đã thực sự xảy ra. Vì vậy, nếu có một loạt các cam kết hợp nhất lộn xộn? Đó là những gì đã xảy ra, và kho lưu trữ nên bảo tồn điều đó cho hậu thế.
Quan điểm ngược lại là lịch sử cam kết là câu chuyện về cách dự án của bạn được tạo ra.
Bạn sẽ không xuất bản bản nháp đầu tiên của một cuốn sách, vì vậy tại sao lại hiển thị công việc lộn xộn của bạn?
Khi bạn đang làm việc trên một dự án, bạn có thể cần một bản ghi về tất cả các bước sai lầm và con đường cụt của mình, nhưng khi đến lúc hiển thị công việc của bạn cho thế giới, bạn có thể muốn kể một câu chuyện mạch lạc hơn về cách đi từ A đến B.
Những người trong phe này sử dụng các công cụ như rebase và filter-branch để viết lại các cam kết của họ trước khi chúng được hợp nhất vào nhánh chính.
Họ sử dụng các công cụ như rebase và filter-branch, để kể câu chuyện theo cách tốt nhất cho những người đọc trong tương lai.
Bây giờ, với câu hỏi liệu sáp nhập hay rebase tốt hơn: hy vọng bạn sẽ thấy rằng nó không đơn giản như vậy. Git là một công cụ mạnh mẽ và cho phép bạn làm nhiều việc với lịch sử của mình, nhưng mọi nhóm và mọi dự án đều khác nhau. Bây giờ bạn đã biết cả hai điều này hoạt động như thế nào, tùy thuộc vào bạn để quyết định cái nào là tốt nhất cho tình huống cụ thể của bạn.
Bạn có thể có được điều tốt nhất của cả hai thế giới: rebase các thay đổi cục bộ trước khi đẩy để làm sạch công việc của bạn, nhưng không bao giờ rebase bất cứ thứ gì bạn đã đẩy đi đâu đó.
Tóm tắt
Chúng ta đã trình bày các thao tác nhánh và hợp nhất cơ bản trong Git. Bạn nên cảm thấy thoải mái khi tạo và chuyển sang nhánh mới, chuyển đổi giữa các nhánh và hợp nhất các nhánh cục bộ. Bạn cũng sẽ biết cách chia sẻ nhánh của mình bằng cách đẩy chúng lên máy chủ chia sẻ, làm việc với người khác trên các nhánh chung và rebase nhánh trước khi chia sẻ. Tiếp theo, chúng ta sẽ tìm hiểu những gì cần thiết để chạy máy chủ lưu trữ kho Git của riêng bạn.
Git trên máy chủ
Ở giai đoạn này, bạn nên đã nắm được hầu hết các thao tác hàng ngày khi dùng Git. Tuy nhiên, để cộng tác với người khác bằng Git, bạn cần có một kho Git từ xa. Dù về mặt kỹ thuật bạn có thể push và pull trực tiếp tới các kho cá nhân của từng người, cách làm này thường không được khuyến khích vì có thể dễ gây nhầm lẫn về công việc của họ nếu không cẩn thận. Hơn nữa, bạn sẽ muốn đồng nghiệp có thể truy cập kho kể cả khi máy của bạn offline — vì vậy một kho chung đáng tin cậy thường hữu ích. Do đó, cách ưu tiên để cộng tác là thiết lập một kho trung gian mà cả hai cùng truy cập, và push/pull từ kho đó.
Việc chạy một máy chủ Git khá đơn giản. Đầu tiên, bạn chọn các giao thức mà máy chủ sẽ hỗ trợ. Phần đầu của chương này sẽ trình bày các giao thức sẵn có và ưu/nhược điểm của từng loại. Các phần tiếp theo sẽ giải thích một số cấu hình điển hình dùng những giao thức đó và cách để máy chủ của bạn chạy với chúng. Cuối cùng, chúng tôi sẽ xem qua một vài lựa chọn dịch vụ lưu trữ nếu bạn không muốn tự quản lý máy chủ và sẵn sàng đặt mã lên máy chủ của bên thứ ba.
Nếu bạn không muốn chạy máy chủ riêng, bạn có thể bỏ qua đến phần cuối chương để xem các lựa chọn thiết lập tài khoản được host rồi tiếp tục sang chương tiếp theo về các luồng làm việc phân tán.
Một kho từ xa thường là một bare repository — một kho Git không có thư mục làm việc.
Vì kho này chỉ được dùng làm điểm cộng tác, không cần phải có snapshot được checkout trên đĩa; nó chỉ chứa dữ liệu Git.
Nói đơn giản, một bare repository chính là nội dung trong thư mục .git của dự án và không có gì khác.
Các Giao thức
Git có thể sử dụng bốn giao thức mạng chính để truyền dữ liệu: Cục bộ (Local), HTTP, Secure Shell (SSH), và Git. Ở đây chúng ta sẽ thảo luận về chúng là gì và trong những trường hợp cơ bản nào bạn sẽ muốn (hoặc không muốn) sử dụng chúng.
Điều quan trọng cần lưu ý là ngoại trừ các giao thức HTTP, tất cả các giao thức này đều yêu cầu Git phải được cài đặt và hoạt động trên máy chủ.
Giao thức Cục bộ
Cơ bản nhất là Giao thức Cục bộ, trong đó kho lưu trữ từ xa nằm trong một thư mục khác trên đĩa. Điều này thường được sử dụng nếu mọi người trong nhóm của bạn có quyền truy cập vào một hệ thống tệp được chia sẻ như một điểm gắn kết NFS, hoặc trong trường hợp ít xảy ra hơn là mọi người đều đăng nhập vào cùng một máy tính. Trường hợp sau không lý tưởng, bởi vì tất cả các kho lưu trữ mã của bạn sẽ nằm trên cùng một máy tính, làm tăng khả năng mất mát thảm khốc.
Nếu bạn có một hệ thống tệp được chia sẻ, bạn có thể sao chép, đẩy đến và kéo từ một kho lưu trữ dựa trên tệp cục bộ. Để sao chép một kho lưu trữ như thế này hoặc để thêm một kho lưu trữ làm điều khiển từ xa vào một dự án hiện có, hãy sử dụng đường dẫn đến kho lưu trữ làm URL. Ví dụ, để sao chép một kho lưu trữ cục bộ, bạn có thể chạy một cái gì đó như thế này:
$ git clone /srv/git/project.git
Hoặc bạn có thể làm điều này:
$ git clone file:///srv/git/project.git
Git hoạt động hơi khác một chút nếu bạn chỉ định rõ ràng file:// ở đầu URL.
Nếu bạn chỉ chỉ định đường dẫn, Git sẽ cố gắng sử dụng các liên kết cứng hoặc sao chép trực tiếp các tệp mà nó cần.
Nếu bạn chỉ định file://, Git sẽ khởi động các quy trình mà nó thường sử dụng để truyền dữ liệu qua mạng, đây là một phương pháp truyền dữ liệu kém hiệu quả hơn nhiều.
Lý do chính để chỉ định tiền tố file:// là nếu bạn muốn một bản sao sạch của kho lưu trữ không có các tham chiếu hoặc đối tượng không cần thiết, thường là sau khi di chuyển từ một VCS khác hoặc một cái gì đó tương tự (xem [ch10-git-internals] để biết các tác vụ bảo trì).
Chúng tôi sẽ sử dụng đường dẫn thông thường ở đây vì làm như vậy gần như luôn nhanh hơn.
Để thêm một kho lưu trữ cục bộ vào một dự án Git hiện có, bạn có thể chạy một cái gì đó như thế này:
$ git remote add local_proj /srv/git/project.git
Sau đó, bạn có thể đẩy đến và kéo từ điều khiển từ xa đó như thể nó đang ở trên mạng.
Ưu điểm
Ưu điểm của các kho lưu trữ dựa trên tệp là chúng đơn giản và chúng sử dụng các quyền truy cập tệp và mạng hiện có. Nếu bạn đã có một hệ thống tệp được chia sẻ mà toàn bộ nhóm của bạn có quyền truy cập, việc thiết lập một kho lưu trữ rất dễ dàng. Bạn đặt bản sao kho lưu trữ trần ở một nơi nào đó mà mọi người đều có quyền truy cập chung và thiết lập quyền đọc/ghi như bạn làm cho bất kỳ thư mục chia sẻ nào khác. Chúng tôi sẽ thảo luận về cách xuất một bản sao kho lưu trữ trần cho mục đích này trong phần tiếp theo, Cài đặt Git trên một Máy chủ.
Đây cũng là một lựa chọn tốt để nhanh chóng lấy công việc từ kho lưu trữ của người khác.
Nếu bạn và một đồng nghiệp đang làm việc trên cùng một dự án và họ muốn bạn kiểm tra một cái gì đó, việc chạy một lệnh như git pull /home/john/project thường dễ dàng hơn là họ đẩy lên một máy chủ từ xa và bạn kéo xuống.
Nhược điểm
Nhược điểm của phương pháp này là quyền truy cập chung thường khó thiết lập và tiếp cận hơn từ nhiều vị trí so với quyền truy cập mạng cơ bản. Nếu bạn muốn đẩy từ máy tính xách tay của mình khi bạn ở nhà, bạn phải gắn kết đĩa từ xa, điều này có thể khó khăn và chậm so với quyền truy cập dựa trên mạng.
Điều quan trọng cần đề cập là đây không nhất thiết là tùy chọn nhanh nhất. Một kho lưu trữ cục bộ chỉ nhanh khi bạn có quyền truy cập nhanh vào dữ liệu. Một kho lưu trữ trên một điểm gắn kết NFS thường chậm hơn kho lưu trữ qua SSH trên cùng một máy chủ, cho phép Git chạy trên các đĩa cục bộ trên mỗi hệ thống.
Cuối cùng, giao thức này không bảo vệ kho lưu trữ khỏi thiệt hại vô tình. Mọi người dùng đều có quyền truy cập shell đầy đủ vào thư mục từ xa, và không có gì ngăn cản họ sửa đổi hoặc xóa các tệp Git nội bộ và làm hỏng kho lưu trữ.
Các Giao thức HTTP
Git qua HTTP rất phổ biến, vì nó có thể được cấu hình để chạy ở hai chế độ khác nhau. HTTP thông minh đã là chế độ mặc định để truyền dữ liệu kể từ phiên bản Git 1.6.6, và hoạt động rất giống với các giao thức SSH hoặc Git, nhưng chạy qua các cổng HTTP/S tiêu chuẩn. HTTP ngu ngốc là một giao thức đơn giản hơn, chỉ đọc và không cần nhiều logic phía máy chủ.
HTTP Thông minh
Giao thức HTTP "Thông minh" chạy qua các cổng HTTP/S thông thường và có thể sử dụng các cơ chế xác thực HTTP khác nhau. Bởi vì nó chạy qua các cổng HTTP/S tiêu chuẩn, nó thường có thể đi qua các tường lửa mà nếu không sẽ chặn các giao thức khác.
Nó đã trở nên rất phổ biến trong vài năm qua, vì nó có thể được thiết lập cho cả quyền truy cập chỉ đọc ẩn danh như giao thức git://, và cho quyền truy cập đẩy được xác thực như giao thức SSH.
Thay vì phải thiết lập các URL riêng biệt cho những thứ này, bây giờ bạn có thể sử dụng một URL duy nhất cho cả hai.
Nếu bạn cố gắng đẩy và kho lưu trữ yêu cầu xác thực (điều này là bình thường), máy chủ có thể nhắc nhập tên người dùng và mật khẩu.
Điều tương tự cũng đúng với quyền truy cập đọc.
Trên thực tế, đối với các dịch vụ như GitHub, URL bạn sử dụng để xem kho lưu trữ trực tuyến (ví dụ: https://github.com/schacon/simplegit) là cùng một URL bạn có thể sử dụng để sao chép, và, nếu bạn có quyền truy cập, để đẩy đến.
Giao thức Ngu ngốc
Nếu máy chủ không phản hồi với một dịch vụ Git HTTP Thông minh, máy khách Git sẽ cố gắng tiến hành với giao thức HTTP "Ngu ngốc" đơn giản hơn.
Giao thức Ngu ngốc mong đợi kho lưu trữ Git trần được phục vụ như các tệp thông thường từ máy chủ web.
Vẻ đẹp của giao thức Ngu ngốc là sự đơn giản của việc thiết lập nó.
Về cơ bản, tất cả những gì bạn phải làm là đặt một kho lưu trữ Git trần dưới gốc tài liệu HTTP của bạn và thiết lập một hook post-update cụ thể (xem [_git_hooks]).
Bây giờ, bất kỳ ai có thể truy cập máy chủ web mà bạn đặt kho lưu trữ cũng có thể sao chép kho lưu trữ của bạn.
Đây là một cách đơn giản để thiết lập quyền truy cập đọc vào một kho lưu trữ qua HTTP Ngu ngốc:
Giả sử bạn có một máy chủ web đang chạy tại git.example.com và các kho lưu trữ Git của bạn nằm trong /srv/git.
Bạn muốn phục vụ chúng từ thư mục con /git.
Đầu tiên, bạn cần tạo thư mục và cho Apache biết đó là một thư mục có thể truy cập web.
Bạn có thể thêm phần sau vào tệp cấu hình Apache của mình (/etc/apache2/httpd.conf trên Ubuntu, chẳng hạn):
<Directory /srv/git>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
Sau đó, bạn có thể đặt kho lưu trữ trần vào thư mục đó và thiết lập hook post-update để nó sẽ cung cấp thông tin cần thiết cho các máy khách HTTP ngu ngốc:
$ cd project.git
$ mv hooks/post-update.sample hooks/post-update
$ chmod a+x hooks/post-update
Hook post-update này làm gì?
Về cơ bản, nó chạy lệnh git update-server-info để đảm bảo rằng việc tìm nạp và sao chép qua HTTP sẽ hoạt động bình thường.
Lệnh này được chạy bất cứ khi nào kho lưu trữ được đẩy đến (qua SSH, chẳng hạn); sau đó, những người khác có thể sao chép bằng cách chạy một cái gì đó như:
$ git clone https://git.example.com/git/project.git
Trong trường hợp cụ thể này, chúng tôi đang sử dụng đường dẫn /git/project.git cho các kho lưu trữ của mình, nhưng bạn có thể thiết lập điều này theo bất kỳ cách nào bạn muốn.
Lệnh git update-server-info được chạy theo mặc định, vì vậy việc thiết lập quyền truy cập dựa trên HTTP theo cách này là siêu dễ dàng.
Bạn cũng có thể làm cho kho lưu trữ này có thể ghi qua HTTP, mặc dù điều đó phức tạp hơn một chút. Chúng tôi sẽ không đề cập đến điều đó ở đây, bởi vì nếu bạn đang ghi vào một kho lưu trữ, việc sử dụng HTTP Thông minh hoặc SSH là một trải nghiệm tốt hơn nhiều.
Ưu điểm
Sự đơn giản của giao thức Ngu ngốc làm cho nó phổ biến. Nếu bạn muốn làm cho một kho lưu trữ chỉ đọc qua HTTP, nó có thể được thiết lập trong vài phút.
HTTP Thông minh có lợi thế là một URL duy nhất cho tất cả các quyền truy cập, mà máy chủ không cần biết trước liệu người dùng sẽ đọc hay ghi. Nó rất nhanh và hiệu quả, và nó đã trở thành phương pháp phổ biến nhất để lưu trữ Git.
Nhược điểm
Nhược điểm chính của HTTP Thông minh là nó không đơn giản để thiết lập ở phía máy chủ. Nó đòi hỏi một chút cấu hình trên máy chủ để chạy đúng cách.
Nhược điểm của giao thức Ngu ngốc là nó, ừm, ngu ngốc.
Nó chậm và không hiệu quả, và yêu cầu máy khách phải làm rất nhiều việc.
Nó cũng tĩnh, có nghĩa là nếu đó là cách duy nhất một kho lưu trữ được phục vụ, bạn sẽ cần chạy git update-server-info trên máy chủ mỗi khi một thay đổi được đẩy để giữ cho nó được cập nhật.
Giao thức SSH
Một giao thức truyền tải phổ biến cho Git khi tự lưu trữ là SSH. Điều này là do quyền truy cập SSH vào các máy chủ đã được thiết lập ở hầu hết các nơi — và nếu không, nó rất dễ thực hiện. SSH cũng là một giao thức mạng được xác thực và, bởi vì nó có mặt ở khắp mọi nơi, nó thường dễ thiết lập và sử dụng.
Để sao chép một kho lưu trữ Git qua SSH, bạn có thể chỉ định một URL ssh:// như sau:
$ git clone ssh://user@server/project.git
Hoặc bạn có thể sử dụng cú pháp giống như scp phổ biến hơn cho giao thức SSH:
$ git clone user@server:project.git
Bạn cũng có thể không chỉ định người dùng, và Git sẽ sử dụng người dùng bạn hiện đang đăng nhập.
Ưu điểm
Ưu điểm của việc sử dụng SSH là rất nhiều. Đầu tiên, SSH tương đối dễ thiết lập — các daemon SSH là phổ biến, nhiều quản trị viên mạng có kinh nghiệm với chúng, và nhiều bản phân phối hệ điều hành được thiết lập với chúng hoặc có các công cụ để quản lý chúng. Thứ hai, quyền truy cập qua SSH là an toàn — tất cả dữ liệu truyền đi đều được mã hóa và xác thực. Cuối cùng, giống như các giao thức HTTP/S, Git, và Cục bộ, SSH hiệu quả, làm cho dữ liệu nhỏ gọn nhất có thể trước khi truyền đi.
Nhược điểm
Mặt tiêu cực của SSH là nó không hỗ trợ quyền truy cập ẩn danh vào kho lưu trữ Git của bạn. Nếu bạn đang sử dụng SSH, người dùng phải có quyền truy cập SSH vào máy của bạn, ngay cả đối với quyền truy cập chỉ đọc, điều này không làm cho SSH có lợi cho các dự án mã nguồn mở. Nếu bạn chỉ sử dụng nó trong mạng công ty của mình, SSH có thể là giao thức duy nhất bạn cần xử lý. Nếu bạn muốn cho phép quyền truy cập chỉ đọc ẩn danh vào các dự án của mình và cũng sử dụng SSH, bạn phải thiết lập SSH để bạn đẩy qua nhưng một cái gì đó khác để người khác kéo từ đó.
Giao thức Git
Cuối cùng chúng ta có giao thức Git.
Đây là một daemon đặc biệt đi kèm với Git; nó lắng nghe trên một cổng chuyên dụng (9418) cung cấp một dịch vụ tương tự như giao thức SSH, nhưng không có xác thực.
Để một kho lưu trữ được phục vụ qua giao thức Git, bạn phải tạo một tệp git-daemon-export-ok — daemon sẽ không phục vụ một kho lưu trữ nếu không có tệp đó trong đó — nhưng ngoài ra không có bảo mật nào khác.
Hoặc là kho lưu trữ Git có sẵn cho mọi người sao chép hoặc không.
Điều này có nghĩa là thường không có việc đẩy qua giao thức này.
Bạn có thể bật quyền truy cập đẩy, nhưng với việc thiếu xác thực, nếu bạn bật quyền truy cập đẩy, bất kỳ ai trên internet tìm thấy URL của dự án của bạn đều có thể đẩy vào dự án của bạn.
Chỉ cần nói rằng, điều này là hiếm.
Ưu điểm
Giao thức Git thường là giao thức truyền tải mạng nhanh nhất hiện có. Nếu bạn đang phục vụ rất nhiều lưu lượng truy cập cho một dự án công cộng hoặc phục vụ một dự án rất lớn không yêu cầu xác thực người dùng để truy cập đọc, có khả năng bạn sẽ muốn thiết lập một daemon Git để phục vụ dự án của mình. Nó sử dụng cùng một cơ chế truyền dữ liệu như giao thức SSH nhưng không có chi phí mã hóa và xác thực.
Nhược điểm
Nhược điểm của giao thức Git là thiếu xác thực.
Thường thì không mong muốn giao thức Git là quyền truy cập duy nhất vào dự án của bạn.
Nói chung, bạn sẽ ghép nối nó với quyền truy cập SSH hoặc HTTPS cho một số ít nhà phát triển có quyền đẩy (ghi) và để mọi người khác sử dụng git:// để truy cập chỉ đọc.
Nó cũng có lẽ là giao thức khó thiết lập nhất.
Nó phải chạy daemon riêng của mình, yêu cầu cấu hình xinetd hoặc systemd hoặc tương tự, điều này không phải lúc nào cũng dễ dàng.
Nó cũng yêu cầu quyền truy cập tường lửa vào cổng 9418, đây không phải là một cổng tiêu chuẩn mà các tường lửa của công ty luôn cho phép.
Đằng sau các tường lửa lớn của công ty, cổng khó hiểu này thường bị chặn.
Cài đặt Git trên một Máy chủ
Bây giờ chúng ta sẽ đề cập đến cách cài đặt Git trên một máy chủ.
|
Chúng ta sẽ đi qua việc thiết lập một máy chủ với giao thức SSH, vì đây là thiết lập phổ biến nhất. SSH cũng là một trong những cách duy nhất để có cả quyền đọc và ghi vào một máy chủ trong khi vẫn đảm bảo an toàn. Các giao thức khác có sẵn, nhưng chúng thường phức tạp hơn để thiết lập, hoặc chỉ có quyền đọc. |
Để thiết lập ban đầu bất kỳ máy chủ Git nào, bạn phải xuất một kho lưu trữ hiện có vào một kho lưu trữ trần mới — một kho lưu trữ không chứa thư mục làm việc.
Điều này thường dễ thực hiện.
Để sao chép một kho lưu trữ để tạo một kho lưu trữ trần mới, bạn chạy lệnh clone với tùy chọn --bare.
Theo quy ước, các thư mục kho lưu trữ trần kết thúc bằng .git, như sau:
$ git clone --bare my_project my_project.git
Cloning into bare repository 'my_project.git'...
done.
Bây giờ bạn sẽ có một bản sao của dữ liệu thư mục Git trong thư mục my_project.git của bạn.
Đầu ra của lệnh git clone --bare hơi khó hiểu.
Vì việc sao chép về cơ bản là một git init sau đó là một git fetch, đầu ra của git init được in ra, tạo ra một thư mục trống.
Các đối tượng thực tế sau đó được chuyển, nhưng không có phản hồi nào được đưa ra.
Tuy nhiên, bạn sẽ có một bản sao đầy đủ của dữ liệu kho lưu trữ Git trong thư mục my_project.git.
Điều này gần tương đương với:
$ cp -Rf my_project/.git my_project.git
mặc dù có một vài khác biệt nhỏ trong tệp cấu hình; nhưng đối với mục đích của bạn, điều này gần giống nhau. Nó chỉ lấy kho lưu trữ Git, không có thư mục làm việc, và tạo ra một thư mục riêng cho nó.
Đặt Kho lưu trữ Trần trên một Máy chủ
Bây giờ bạn đã có một bản sao trần của kho lưu trữ của mình, tất cả những gì bạn cần làm là đặt nó trên một máy chủ và thiết lập các giao thức của bạn.
Giả sử bạn đã thiết lập một máy chủ có tên git.example.com mà bạn có quyền truy cập SSH và bạn muốn lưu trữ tất cả các kho lưu trữ Git của mình trong thư mục /srv/git.
Giả sử rằng /srv/git tồn tại trên máy chủ đó, bạn có thể thiết lập kho lưu trữ mới của mình bằng cách sao chép kho lưu trữ trần của bạn qua:
$ scp -r my_project.git user@git.example.com:/srv/git
Tại thời điểm này, những người dùng khác có quyền truy cập SSH vào cùng một máy chủ có quyền đọc vào thư mục /srv/git có thể sao chép kho lưu trữ của bạn bằng cách chạy:
$ git clone user@git.example.com:/srv/git/my_project.git
Nếu một người dùng, có quyền truy cập SSH vào một máy chủ, có quyền ghi vào thư mục /srv/git/my_project.git, họ cũng sẽ tự động có quyền đẩy.
Git sẽ tự động thêm quyền ghi nhóm vào một kho lưu trữ nếu bạn chạy lệnh git init với tùy chọn --shared.
$ ssh user@git.example.com
$ cd /srv/git/my_project.git
$ git init --bare --shared
Bạn thấy việc lấy một kho lưu trữ Git, tạo một phiên bản trần của nó và đặt nó trên một máy chủ mà bạn và các cộng tác viên của bạn có quyền truy cập SSH dễ dàng như thế nào. Bây giờ bạn đã sẵn sàng để cộng tác trên cùng một dự án.
Điều quan trọng cần lưu ý là đây thực sự là tất cả những gì bạn cần làm để chạy một máy chủ Git hữu ích mà nhiều người có quyền truy cập — chỉ cần thêm các tài khoản có thể SSH trên một máy chủ, và đặt một kho lưu trữ trần ở đâu đó mà tất cả những người dùng đó đều có quyền đọc và ghi. Bạn đã sẵn sàng — không cần gì khác.
Trong vài phần tiếp theo, bạn sẽ thấy cách mở rộng điều này. Chúng ta sẽ đề cập đến cách thiết lập quyền truy cập đọc ẩn danh, cách thêm giao diện web, sử dụng một công cụ có tên là GitLab, và nhiều hơn nữa. Tuy nhiên, hãy nhớ rằng để cộng tác với một vài người trong một dự án riêng tư, một máy chủ SSH và một kho lưu trữ trần là tất cả những gì bạn cần.
Các Thiết lập Nhỏ
Nếu bạn là một tổ chức nhỏ hoặc chỉ đang thử nghiệm Git trong tổ chức của mình và chỉ có một vài nhà phát triển, mọi thứ có thể đơn giản cho bạn. Một trong những khía cạnh phức tạp nhất của việc thiết lập một máy chủ Git là quản lý người dùng. Nếu bạn muốn một số kho lưu trữ chỉ đọc đối với một số người dùng và đọc/ghi đối với những người khác, việc sắp xếp quyền truy cập và quyền có thể hơi khó khăn hơn.
Truy cập SSH
Nếu bạn có một máy chủ mà tất cả các nhà phát triển của bạn đã có quyền truy cập SSH, thường thì việc thiết lập kho lưu trữ đầu tiên của bạn ở đó là dễ nhất, bởi vì bạn gần như không phải làm gì cả (như chúng ta đã đề cập trong phần cuối). Nếu bạn muốn có các quyền kiểm soát truy cập phức tạp hơn trên các kho lưu trữ của mình, bạn có thể xử lý chúng bằng các quyền hệ thống tệp thông thường của hệ điều hành mà máy chủ của bạn đang chạy.
Nếu bạn muốn đặt các kho lưu trữ của mình trên một máy chủ không có tài khoản cho mọi người trong nhóm của bạn mà bạn muốn có quyền ghi, thì bạn phải thiết lập quyền truy cập SSH cho họ. Chúng tôi giả định rằng nếu bạn có một máy chủ để làm điều này, bạn đã có một máy chủ SSH được cài đặt và đang chạy.
Tạo Khóa Công khai SSH
Nhiều máy chủ Git xác thực bằng khóa công khai SSH.
Để cung cấp khóa công khai, mỗi người dùng trong hệ thống của bạn phải tạo một khóa nếu họ chưa có.
Quá trình này tương tự nhau trên tất cả các hệ điều hành.
Đầu tiên, bạn nên kiểm tra xem bạn đã có khóa chưa.
Theo mặc định, các khóa SSH của người dùng được lưu trữ trong thư mục ~/.ssh của người dùng đó.
Bạn có thể dễ dàng kiểm tra xem bạn đã có khóa chưa bằng cách vào thư mục đó và liệt kê nội dung:
$ cd ~/.ssh
$ ls
authorized_keys2 id_dsa known_hosts
config id_dsa.pub
Bạn đang tìm kiếm một cặp tệp có tên giống như id_dsa hoặc id_rsa và một tệp phù hợp có phần mở rộng .pub.
Tệp .pub là khóa công khai của bạn và tệp còn lại là khóa riêng tư của bạn.
Nếu bạn không có các tệp này (hoặc bạn thậm chí không có thư mục .ssh), bạn có thể tạo chúng bằng cách chạy chương trình có tên ssh-keygen, được cung cấp cùng với gói SSH trên các hệ thống Linux/Mac và đi kèm với Git cho Windows:
$ ssh-keygen -o
Generating public/private rsa key pair.
Enter file in which to save the key (/home/schacon/.ssh/id_rsa):
Created directory '/home/schacon/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/schacon/.ssh/id_rsa.
Your public key has been saved in /home/schacon/.ssh/id_rsa.pub.
The key fingerprint is:
d0:82:24:8e:d7:f1:bb:9b:33:53:96:93:49:da:9b:e3 schacon@mylaptop.local
Đầu tiên, nó xác nhận nơi bạn muốn lưu khóa (.ssh/id_rsa), và sau đó nó hỏi mật khẩu hai lần, bạn có thể để trống nếu bạn không muốn nhập mật khẩu khi sử dụng khóa.
Tuy nhiên, nếu bạn sử dụng mật khẩu, hãy đảm bảo thêm tùy chọn -o, tùy chọn này lưu khóa riêng tư ở định dạng có khả năng chống lại việc bẻ khóa mật khẩu brute-force tốt hơn so với định dạng mặc định.
Bạn cũng có thể sử dụng công cụ ssh-agent để tránh phải nhập mật khẩu mỗi lần.
Bây giờ, mỗi người dùng làm điều này phải gửi khóa công khai của họ cho bạn hoặc bất kỳ ai đang quản trị máy chủ Git (giả sử bạn đang sử dụng thiết lập máy chủ SSH yêu cầu khóa công khai).
Tất cả những gì họ phải làm là sao chép nội dung của tệp .pub và gửi email cho bạn.
Các khóa công khai trông giống như thế này:
$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU
GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3
Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA
t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En
mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b/gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx
NrRFi9wrf+M7Q== schacon@mylaptop.local
Để có hướng dẫn sâu hơn về cách tạo khóa SSH trên nhiều hệ thống, hãy xem hướng dẫn GitHub về khóa SSH tại https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent.
Thiết lập Máy chủ
Hãy cùng đi qua việc thiết lập quyền truy cập SSH ở phía máy chủ.
Trong ví dụ này, bạn sẽ sử dụng phương pháp authorized_keys để xác thực người dùng của mình.
Chúng tôi cũng giả định bạn đang chạy một bản phân phối Linux tiêu chuẩn như Ubuntu.
|
Một phần lớn những gì được mô tả ở đây có thể được tự động hóa bằng cách sử dụng lệnh |
Đầu tiên, bạn tạo một tài khoản người dùng git và một thư mục .ssh cho người dùng đó.
$ sudo adduser git
$ su git
$ cd
$ mkdir .ssh && chmod 700 .ssh
$ touch .ssh/authorized_keys && chmod 600 .ssh/authorized_keys
Tiếp theo, bạn cần thêm một số khóa công khai SSH của nhà phát triển vào tệp authorized_keys cho người dùng git.
Giả sử bạn có một số khóa công khai đáng tin cậy và đã lưu chúng vào các tệp tạm thời.
Một lần nữa, các khóa công khai trông giống như thế này:
$ cat /tmp/id_rsa.john.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCB007n/ww+ouN4gSLKssMxXnBOvf9LGt4L
ojG6rs6hPB09j9R/T17/x4lhJA0F3FR1rP6kYBRsWj2aThGw6HXLm9/5zytK6Ztg3RPKK+4k
Yjh6541NYsnEAZuXz0jTTyAUfrtU3Z5E003C4oxOj6H0rfIF1kKI9MAQLMdpGW1GYEIgS9Ez
Sdfd8AcCIicTDWbqLAcU4UpkaX8KyGlLwsNuuGztobF8m72ALC/nLF6JLtPofwFBlgc+myiv
O7TCUSBdLQlgMVOFq1I2uPWQOkOWQAHukEOmfjy2jctxSDBQ220ymjaNsHT4kgtZg2AYYgPq
dAv8JggJICUvax2T9va5 gsg-keypair
Bạn chỉ cần nối chúng vào tệp authorized_keys của người dùng git trong thư mục .ssh của nó:
$ cat /tmp/id_rsa.john.pub >> ~/.ssh/authorized_keys
$ cat /tmp/id_rsa.josie.pub >> ~/.ssh/authorized_keys
$ cat /tmp/id_rsa.jessica.pub >> ~/.ssh/authorized_keys
Bây giờ, bạn có thể thiết lập một kho lưu trữ trống cho họ bằng cách chạy git init với tùy chọn --bare, tùy chọn này khởi tạo kho lưu trữ mà không có thư mục làm việc:
$ cd /srv/git
$ mkdir project.git
$ cd project.git
$ git init --bare
Sau đó, John, Josie, hoặc Jessica có thể đẩy phiên bản đầu tiên của dự án của họ lên máy chủ bằng cách thêm nó làm một điều khiển từ xa và đẩy lên một nhánh.
Lưu ý rằng ai đó phải shell vào máy và tạo một kho lưu trữ trần cho gần như mọi dự án.
Hãy sử dụng project.git cho tên dự án.
# trên máy của John
$ cd myproject
$ git init
$ git add .
$ git commit -m 'Initial commit'
$ git remote add origin git@gitserver:/srv/git/project.git
$ git push origin master
Tại thời điểm này, những người khác có thể sao chép nó xuống và đẩy các thay đổi trở lại dễ dàng như nhau:
$ git clone git@gitserver:/srv/git/project.git
$ cd project
$ vim README
$ git commit -am 'Fix for README file'
$ git push origin master
Với phương pháp này, bạn có thể nhanh chóng có một máy chủ Git đọc/ghi hoạt động cho một vài nhà phát triển.
Một biện pháp phòng ngừa quan trọng cần thực hiện là hạn chế người dùng git chỉ trong các hoạt động Git.
Cách dễ nhất để làm điều đó là thay đổi shell đăng nhập của người dùng git thành một công cụ có tên git-shell đi kèm với Git.
Nếu bạn đặt đây làm shell đăng nhập của người dùng git, thì người dùng đó sẽ không thể có quyền truy cập shell bình thường vào máy chủ của bạn.
Để làm điều này, hãy chỉ định git-shell thay vì bash hoặc csh cho shell đăng nhập của người dùng của bạn.
Bạn thường có thể tìm thấy đường dẫn đầy đủ đến lệnh git-shell bằng cách xem trong tệp /etc/shells của bạn:
$ cat /etc/shells
/bin/sh
/bin/bash
/sbin/nologin
/bin/dash
/bin/zsh
/bin/tcsh
/usr/bin/git-shell
Sau đó, bạn có thể thay đổi shell cho một người dùng bằng lệnh chsh <username>.
Bây giờ người dùng git vẫn có thể sử dụng kết nối SSH để đẩy và kéo từ các kho lưu trữ Git nhưng không thể có được một shell máy.
Nếu họ cố gắng, họ sẽ thấy một từ chối đăng nhập như thế này:
$ ssh git@gitserver
fatal: Interactive git shell is not enabled.
hint: ~git/git-shell-commands should exist and have read and execute access.
Connection to gitserver closed.
Bây giờ người dùng Git có thể sử dụng kết nối SSH của họ cho những việc khác, ví dụ như thiết lập chuyển tiếp cổng.
Để không cho phép điều đó, bạn có thể chỉnh sửa tệp authorized_keys và thêm một số tùy chọn vào đầu mỗi khóa bạn muốn hạn chế.
Các tùy chọn này là (trong số những tùy chọn khác):
-
no-port-forwarding -
no-X11-forwarding -
no-agent-forwarding -
no-pty
Điều này cho phép người dùng kết nối với máy chủ để đẩy và kéo từ các kho lưu trữ Git, nhưng không có gì khác. Đây là một ví dụ:
no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa
AAAAB3NzaC1yc2EAAAADAQABAAABAQDEwENNMomTboYI+LJieaAY16qiXiH3wuvENhBG...
Bây giờ các lệnh mạng Git sẽ vẫn hoạt động tốt nhưng người dùng sẽ không thể có được một shell.
Như đầu ra đã nêu, bạn cũng có thể thiết lập một thư mục trong thư mục chính của người dùng git để tùy chỉnh lệnh git-shell một chút.
Ví dụ, bạn có thể hạn chế các lệnh Git mà máy chủ sẽ chấp nhận hoặc bạn có thể tùy chỉnh thông báo mà người dùng nhìn thấy nếu họ cố gắng SSH như vậy.
Chạy git help shell để biết thêm thông tin về cách tùy chỉnh shell.
Git Daemon
Tiếp theo, bạn sẽ thiết lập một máy chủ dựa trên daemon. Đây là một lựa chọn phổ biến để truy cập nhanh, không cần xác thực vào dữ liệu Git của bạn. Hãy nhớ rằng vì đây không phải là một dịch vụ được xác thực, bất cứ thứ gì bạn phục vụ qua giao thức này đều là công khai trong mạng của nó.
Nếu bạn đang chạy cái này trên một máy chủ bên ngoài tường lửa của mình, nó chỉ nên được sử dụng cho các dự án có thể nhìn thấy công khai trên toàn thế giới. Nếu bạn đang chạy nó bên trong tường lửa của mình, bạn có thể sử dụng nó cho các dự án mà một số lượng lớn người hoặc máy tính (máy chủ tích hợp liên tục hoặc xây dựng) có quyền truy cập chỉ đọc, khi bạn không muốn phải quản lý một khóa SSH cho mỗi người.
Trong mọi trường hợp, giao thức Git tương đối dễ thiết lập. Về cơ bản, bạn cần chạy lệnh này dưới dạng một daemon:
$ git daemon --reuseaddr --base-path=/srv/git/ /srv/git/
--reuseaddr cho phép máy chủ khởi động lại mà không cần chờ các kết nối cũ hết thời gian, trong khi --base-path cho phép mọi người sao chép các dự án mà không cần phải chỉ định toàn bộ đường dẫn.
Đường dẫn ở cuối cho daemon Git biết nơi tìm kiếm các kho lưu trữ để xuất.
Nếu bạn đang chạy tường lửa, bạn cũng sẽ cần phải tạo một lỗ hổng trong đó tại cổng 9418 trên hộp bạn đang thiết lập cái này.
Bạn có thể biến quá trình này thành daemon theo một số cách, tùy thuộc vào hệ điều hành bạn đang chạy. Trên máy Ubuntu, bạn có thể sử dụng một tập lệnh Upstart. Vì vậy, trong tệp sau:
/etc/init/git-daemon.conf
bạn có thể đặt tập lệnh này:
start on startup
stop on shutdown
exec /usr/bin/git daemon --user=git --group=git \
--reuseaddr \
--base-path=/srv/git/ \
/srv/git/
respawn
Vì lý do bảo mật, rất khuyến khích để daemon này chạy với tư cách người dùng có quyền chỉ đọc đối với các kho lưu trữ — bạn có thể dễ dàng thực hiện điều này bằng cách tạo người dùng mới git-ro và chạy daemon với tư cách là họ.
Để đơn giản, chúng tôi sẽ chỉ chạy nó với tư cách là người dùng git giống như git-shell đang chạy.
|
Khi bạn khởi động lại máy chủ của mình, daemon Git của bạn cũng sẽ khởi động lại. Để khởi động hoặc dừng nó theo cách thủ công, bạn có thể sử dụng các lệnh:
|
Trên các hệ thống khác, bạn có thể muốn sử dụng xinetd, một tập lệnh trong hệ thống sysvinit của bạn, hoặc một cái gì đó khác — miễn là bạn biến lệnh đó thành daemon và được theo dõi bằng cách nào đó.
Tiếp theo, bạn phải cho Git biết kho lưu trữ nào được phép truy cập dựa trên máy chủ Git không được xác thực.
Bạn có thể làm điều này trong mỗi kho lưu trữ bằng cách tạo một tệp có tên git-daemon-export-ok.
$ cd /path/to/project.git
$ touch git-daemon-export-ok
Sự hiện diện của tệp đó cho Git biết rằng có thể phục vụ dự án này mà không cần xác thực.
HTTP Thông minh
Bây giờ chúng ta đã đề cập đến những điều cơ bản về các giao thức Git, SSH và Cục bộ. Tại thời điểm này, bạn có thể thiết lập và chạy một máy chủ Git có quyền đọc/ghi.
Tuy nhiên, có một tùy chọn khác để truy cập mạng: HTTP. Giao thức HTTP thường bị bỏ qua, vì nó thường đòi hỏi một chút thiết lập ở phía máy chủ. Tuy nhiên, trong vài năm qua, một số máy chủ Git đã xuất hiện đã làm cho việc hỗ trợ HTTP Thông minh trở nên phổ biến hơn nhiều.
Vì đây là một giao thức không trạng thái chung, HTTP thông thường có thể không hiệu quả để truyền dữ liệu Git. Tuy nhiên, có thể làm cho việc truyền dữ liệu thông minh hơn, bằng cách để máy chủ diễn giải một cách thông minh các yêu cầu của máy khách. Đây là ý nghĩa của "HTTP Thông minh".
Để thiết lập một kho lưu trữ Git được phục vụ qua HTTP Thông minh, bạn cần bật một tập lệnh CGI được cung cấp cùng với Git có tên là git-http-backend.
Tập lệnh này sẽ đọc đường dẫn và các tiêu đề của yêu cầu được gửi bởi git fetch hoặc git push đến một URL, và xác định xem máy khách đang cố gắng làm gì.
Nếu tập lệnh CGI thấy rằng máy khách đang cố gắng tìm nạp hoặc đẩy, nó sẽ giao tiếp với máy khách qua một giao thức thông minh.
Thiết lập
Để thiết lập CGI git-http-backend, trước tiên bạn sẽ cần một máy chủ web có thể chạy các tập lệnh CGI.
Trong ví dụ này, chúng tôi sẽ sử dụng Apache.
Bạn sẽ cần bật các mô-đun mod_cgi, mod_alias, và mod_env.
$ sudo a2enmod cgi alias env
Bạn cũng sẽ cần đặt biến môi trường GIT_PROJECT_ROOT, để git-http-backend biết nơi tìm các kho lưu trữ.
Đây phải là gốc của các kho lưu trữ Git của bạn — thư mục mà bạn có các thư mục project.git.
SetEnv GIT_PROJECT_ROOT /srv/git
Tiếp theo, bạn sẽ cần cấu hình Apache để chạy tập lệnh git-http-backend khi bạn truy cập một đường dẫn cụ thể trên máy chủ web của mình.
ScriptAlias /git/ /usr/lib/git-core/git-http-backend/
Cuối cùng, bạn có thể muốn cho phép ghi vào kho lưu trữ, điều mà bạn có thể làm với một khối cấu hình như thế này:
<Location /git/project.git>
AuthType Basic
AuthName "Git Access"
AuthUserFile /srv/git/.htpasswd
Require valid-user
</Location>
Điều này sẽ yêu cầu bạn tạo một tệp .htpasswd chứa mật khẩu của tất cả những người dùng hợp lệ.
Đây là một ví dụ về việc thêm người dùng schacon vào tệp:
$ sudo htpasswd -c /srv/git/.htpasswd schacon
Có rất nhiều cách để Apache xác thực người dùng, vì vậy bạn có thể tìm ra cách tích hợp với thiết lập xác thực hiện có của mình.
Bạn cũng có thể cung cấp quyền truy cập chỉ đọc bằng cách xóa phần Require valid-user, nhưng bạn vẫn cần cho phép quyền truy cập để đẩy theo một cách khác.
Một cách để làm điều đó là yêu cầu xác thực để đẩy, nhưng cho phép đọc cho mọi người, với một khối như thế này:
<Files "git-http-backend">
AuthType Basic
AuthName "Git Access"
AuthUserFile /srv/git/.htpasswd
Require expr !(%{QUERY_STRING} -strmatch '*service=git-receive-pack*' || %{REQUEST_URI} =~ m#/git-receive-pack$#)
Require valid-user
</Files>
GitWeb
Bây giờ bạn đã có quyền truy cập đọc/ghi và chỉ đọc cơ bản vào dự án của mình, bạn có thể muốn thiết lập một trình hiển thị dựa trên web đơn giản. Git đi kèm với một tập lệnh CGI sẵn có cho mục đích này có tên là GitWeb. Bạn có thể thấy GitWeb đang hoạt động tại những nơi như https://git.kernel.org.
Nếu bạn muốn kiểm tra xem GitWeb trông như thế nào đối với dự án của mình, Git đi kèm với một lệnh để khởi động một phiên bản tạm thời ngay lập tức nếu bạn có một máy chủ web nhẹ như lighttpd hoặc webrick trên hệ thống của mình.
Trên các máy Linux, bạn thường có thể tìm thấy lighttpd được cài đặt, vì vậy bạn có thể chạy nó bằng cách gõ git instaweb trong thư mục dự án của mình.
Nếu bạn đang chạy máy Mac, Leopard đi kèm với Ruby được cài đặt sẵn, vì vậy webrick có thể là lựa chọn tốt nhất của bạn.
Để khởi động instaweb với trình xử lý không phải là lighttpd, bạn có thể chạy nó với tùy chọn --httpd.
$ git instaweb --httpd=webrick
[2009-02-21 10:02:21] INFO WEBrick 1.3.1
[2009-02-21 10:02:21] INFO ruby 1.8.6 (2008-08-11) [universal-darwin9.0]
Lệnh này khởi động máy chủ HTTP trên cổng 1234 và sau đó tự động khởi động trình duyệt web mở trang đó.
Nó khá dễ dàng cho bạn.
Khi bạn hoàn tất và muốn khởi động lại máy chủ, bạn có thể chạy cùng một lệnh với tùy chọn --stop:
$ git instaweb --httpd=webrick --stop
Nếu bạn muốn chạy giao diện web này mọi lúc trên máy chủ cho nhóm của mình hoặc cho một dự án mã nguồn mở bạn đang lưu trữ, bạn sẽ cần thiết lập tập lệnh CGI để được phục vụ bởi máy chủ web bình thường của mình.
Một số bản phân phối Linux có gói gitweb mà bạn có thể cài đặt thông qua apt hoặc yum, vì vậy bạn có thể muốn thử điều đó trước.
Chúng tôi sẽ xem xét việc cài đặt GitWeb thủ công rất nhanh.
Đầu tiên, bạn cần lấy mã nguồn Git, đi kèm với GitWeb, và tạo tập lệnh CGI tùy chỉnh:
$ git clone git://git.kernel.org/pub/scm/git/git.git
$ cd git/
$ make GITWEB_PROJECTROOT="/opt/git" prefix=/usr gitweb
$ sudo cp -Rf gitweb /var/www/
Lưu ý rằng bạn phải cho lệnh make biết nơi tìm các kho lưu trữ Git của mình bằng biến GITWEB_PROJECTROOT.
Bây giờ, bạn cần làm cho Apache sử dụng CGI cho tập lệnh đó, bạn có thể thêm VirtualHost cho nó:
<VirtualHost *:80>
ServerName gitserver
DocumentRoot /var/www/gitweb
<Directory /var/www/gitweb>
Options +ExecCGI +FollowSymLinks +SymLinksIfOwnerMatch
AllowOverride All
order allow,deny
Allow from all
AddHandler cgi-script cgi
DirectoryIndex gitweb.cgi
</Directory>
</VirtualHost>
Một lần nữa, GitWeb có thể được phục vụ bằng bất kỳ máy chủ web nào có khả năng CGI; nếu bạn thích sử dụng thứ khác, nó sẽ không khó để thiết lập.
Tại thời điểm này, bạn sẽ có thể truy cập http://gitserver/ để xem các kho lưu trữ của mình trực tuyến.
GitLab
Giải pháp Git tự lưu trữ phổ biến nhất cho đến nay là GitLab. GitLab là một ứng dụng web lõi mở để cộng tác trên mã, tương tự như GitHub hoặc BitBucket. Đó là một ứng dụng bạn chạy trên phần cứng của riêng mình, vì vậy bạn hoàn toàn kiểm soát nó; bạn có thể chạy nó trên một bản phân phối Linux tiêu chuẩn hoặc một container Docker. Sự khác biệt chính giữa GitLab và các giải pháp khác là nó là lõi mở (vì vậy nếu bạn có khuynh hướng, bạn có thể giúp phát triển hoặc sửa đổi nó) và nó đi kèm với rất nhiều tính năng mà nếu không sẽ yêu cầu tích hợp với các sản phẩm bên ngoài.
GitLab có một giải pháp quản lý kho lưu trữ đầy đủ tính năng, bao gồm kiểm soát truy cập chi tiết, giao diện web đẹp để xem xét mã và yêu cầu hợp nhất, theo dõi vấn đề, wiki và một hệ thống tích hợp liên tục (CI) mạnh mẽ được tích hợp sẵn. Hầu hết những điều này đều có sẵn trong phiên bản “Community Edition” mã nguồn mở, nhưng có nhiều tính năng hơn và hợp đồng hỗ trợ có sẵn với phiên bản “Enterprise Edition”.
Việc thiết lập khá đơn giản. Có một số tùy chọn tải xuống tại https://about.gitlab.com/downloads/, trong đó phổ biến nhất là gói Omnibus, chứa tất cả các dịch vụ cần thiết trong một gói duy nhất. Khi bạn đã chạy nó, hầu hết việc quản trị được thực hiện thông qua giao diện web.
Quản lý người dùng của GitLab khá đơn giản. Người dùng có thể là người dùng chung với không gian tên cá nhân cho các dự án của riêng họ, hoặc quản trị viên có quyền truy cập vào mọi thứ. Người dùng có thể được tạo bởi một quản trị viên, hoặc họ có thể tự đăng ký tài khoản của riêng mình. Người dùng có thể được cấp quyền truy cập vào các dự án trên cơ sở cá nhân, hoặc là một phần của một nhóm.
Các Nhóm
Một nhóm GitLab là một tập hợp các dự án, cùng với dữ liệu về cách người dùng có thể truy cập các dự án đó.
Mỗi nhóm có một không gian tên dự án (giống như cách người dùng làm), vì vậy nếu nhóm training có một dự án materials, URL của nó sẽ là http://server/training/materials.
Mỗi nhóm được liên kết với một số người dùng, mỗi người dùng có một mức độ quyền hạn đối với các dự án của nhóm và chính nhóm đó. Các quyền này từ "Guest" (vấn đề và trò chuyện) đến "Owner" (kiểm soát hoàn toàn). Các mức độ quyền hạn khác nhau được ghi lại khá chi tiết trong tài liệu GitLab.
Các Dự án
Một dự án GitLab là một kho lưu trữ Git duy nhất. Nó thuộc về một không gian tên duy nhất, hoặc là người dùng hoặc là nhóm. Nếu nó thuộc về một người dùng, những người duy nhất có quyền truy cập theo mặc định là chủ sở hữu dự án và các quản trị viên hệ thống. Nếu nó thuộc về một nhóm, tất cả người dùng được liên kết với nhóm đó sẽ có quyền truy cập, theo mức độ quyền hạn của họ.
Các dự án có thể được đánh dấu là "Private", "Internal", hoặc "Public".
-
Các dự án công khai (Public) có thể được nhìn thấy bởi mọi người, và có thể được sao chép bởi bất kỳ ai mà không cần xác thực.
-
Các dự án nội bộ (Internal) có thể được nhìn thấy bởi bất kỳ người dùng nào đã đăng nhập.
-
Các dự án riêng tư (Private) chỉ có thể được nhìn thấy bởi các thành viên của chúng (hoặc là một cách rõ ràng, hoặc là một phần của một nhóm).
Quyền truy cập được kiểm soát bằng các khóa SSH (như chúng ta đã thảo luận trước đó trong chương), hoặc thông qua tên người dùng và mật khẩu (có thể được tích hợp với một máy chủ thư mục LDAP hoặc công ty khác).
Cộng tác
Bây giờ bạn đã thiết lập một dự án, bạn có thể sẽ muốn thực hiện một số cộng tác trên đó. Một dự án trong GitLab chỉ là một kho lưu trữ Git thông thường, vì vậy bạn có thể sao chép nó, đẩy lên nó, v.v. Giao diện web cũng cung cấp một số công cụ để giúp bạn cộng tác.
Mỗi dự án có một bức tường, hơi giống như dòng thời gian trên một dịch vụ truyền thông xã hội. Đó là một bảng điều khiển về hoạt động gần đây trong dự án.
Mỗi dự án cũng có một trình theo dõi vấn đề, nơi bạn có thể báo cáo các vấn đề với mã hoặc bắt đầu các cuộc thảo luận.
Tính năng phổ biến nhất của GitLab cho đến nay là yêu cầu hợp nhất. Đó là một cách để thảo luận về các thay đổi được đề xuất trong mã, và tương tự như Yêu cầu Kéo của GitHub hoặc xem xét thay đổi của Gerrit. Nó cho phép bất kỳ người dùng nào có thể xem một dự án tạo một bản sao của nó, đẩy các thay đổi vào bản sao đó, và sau đó mở một yêu cầu để các thay đổi của họ được hợp nhất vào dự án chính. Tác giả ban đầu có thể được chỉ định cho yêu cầu, và một cuộc thảo luận có thể diễn ra xung quanh thay đổi cho đến khi tác giả hài lòng với nó và hợp nhất thay đổi.
Git Được Lưu trữ (Hosted Git)
Nếu bạn không muốn trải qua tất cả công việc liên quan đến việc thiết lập máy chủ Git của riêng mình, bạn có một số tùy chọn để lưu trữ các dự án Git của mình trên một trang web lưu trữ chuyên dụng bên ngoài. Làm như vậy mang lại một số lợi thế: một trang web lưu trữ thường nhanh chóng để thiết lập và dễ dàng bắt đầu các dự án và không cần bảo trì máy chủ hoặc giám sát. Ngay cả khi bạn thiết lập và chạy máy chủ nội bộ của riêng mình, bạn vẫn có thể muốn sử dụng trang web lưu trữ công cộng cho mã nguồn mở của mình — thường dễ dàng hơn cho cộng đồng mã nguồn mở tìm thấy và giúp bạn với nó theo cách đó.
Ngày nay, bạn có một số lượng lớn các tùy chọn lưu trữ để lựa chọn, mỗi tùy chọn có các ưu điểm và nhược điểm khác nhau. Để xem danh sách cập nhật, hãy xem trang GitHosting trên wiki Git chính tại https://git.wiki.kernel.org/index.php/GitHosting Bởi vì chúng tôi không thể bao gồm tất cả chúng, và bởi vì tôi tình cờ làm việc tại một trong số chúng, chúng tôi sẽ sử dụng phần này để hướng dẫn cách thiết lập tài khoản và tạo dự án mới tại GitHub. Điều này sẽ cung cấp cho bạn ý tưởng về những gì liên quan. Chương tiếp theo dành riêng để thảo luận về GitHub rất chi tiết, vì vậy chúng tôi sẽ không đi sâu vào các tính năng cụ thể của GitHub ở đây, mà thay vào đó tập trung vào cách thiết lập và chia sẻ kho lưu trữ Git.
Tóm tắt
Bạn có nhiều lựa chọn để thiết lập một kho Git từ xa để cộng tác hoặc chia sẻ công việc.
Tự chạy máy chủ cho bạn nhiều quyền kiểm soát và cho phép chạy trong firewall của tổ chức, nhưng thường tốn thời gian thiết lập và bảo trì. Nếu đặt dữ liệu trên dịch vụ host, việc thiết lập và bảo trì dễ dàng hơn; tuy nhiên bạn phải chấp nhận lưu mã trên máy chủ của bên khác, điều mà một số tổ chức không cho phép.
Bây giờ bạn nên có đủ thông tin để chọn giải pháp (hoặc kết hợp giải pháp) phù hợp với bạn và tổ chức của bạn.
Git phân tán
Bây giờ khi bạn đã có một kho Git từ xa làm điểm chung để các nhà phát triển chia sẻ mã và đã quen với các lệnh Git cơ bản trong luồng làm việc cục bộ, bạn sẽ tìm hiểu cách tận dụng một số luồng làm việc phân tán mà Git cung cấp.
Trong chương này, bạn sẽ thấy cách làm việc với Git trong môi trường phân tán với vai trò là một người đóng góp và một người tích hợp. Tức là bạn sẽ học cách đóng góp mã hiệu quả vào một dự án và làm cho việc đó dễ dàng hơn cho bạn và người duy trì dự án, đồng thời biết cách quản lý một dự án khi nhiều nhà phát triển cùng đóng góp.
Các Quy trình làm việc Phân tán
Không giống như các Hệ thống Kiểm soát Phiên bản Tập trung (CVCS), bản chất phân tán của Git cho phép bạn linh hoạt hơn nhiều trong cách các nhà phát triển cộng tác trong các dự án. Trong các hệ thống tập trung, mỗi nhà phát triển là một nút làm việc ít nhiều theo mô hình trung tâm và nan hoa. Tuy nhiên, trong Git, mỗi nhà phát triển có khả năng vừa là một nút vừa là một trung tâm; nghĩa là, mỗi nhà phát triển vừa có thể đóng góp mã cho các kho lưu trữ khác vừa có thể duy trì một kho lưu trữ công khai mà những người khác có thể dựa vào công việc của họ và họ có thể đóng góp vào đó. Điều này mở ra một loạt các khả năng quy trình làm việc cho dự án và/hoặc nhóm của bạn, vì vậy chúng tôi sẽ đề cập đến một vài mô hình phổ biến tận dụng sự linh hoạt này.
Quy trình làm việc Tập trung
Trong các hệ thống tập trung, thường có một mô hình cộng tác duy nhất — quy trình làm việc tập trung. Một trung tâm, hoặc kho lưu trữ, có thể chấp nhận mã, và mọi người đồng bộ hóa công việc của họ với nó. Một số nhà phát triển là các nút — người tiêu dùng của trung tâm đó — và đồng bộ hóa với một nơi đó.
Điều này có nghĩa là nếu hai nhà phát triển sao chép từ trung tâm và cả hai đều thực hiện các thay đổi, nhà phát triển đầu tiên đẩy các thay đổi của họ trở lại có thể làm như vậy mà không có vấn đề gì. Nhà phát triển thứ hai phải hợp nhất công việc của người đầu tiên trước khi đẩy các thay đổi lên, để không ghi đè lên các thay đổi của nhà phát triển đầu tiên. Khái niệm này đúng trong Git cũng như trong Subversion (hoặc bất kỳ CVCS nào), và mô hình này hoạt động hoàn hảo trong Git.
Nếu bạn có một nhóm nhỏ hoặc đã quen với quy trình làm việc tập trung trong công ty của mình, bạn có thể dễ dàng tiếp tục sử dụng quy trình làm việc đó với Git. Chỉ cần thiết lập một kho lưu trữ duy nhất, và cấp cho mọi người trong nhóm của bạn quyền đẩy; Git sẽ không để người dùng ghi đè lên nhau. Nếu một nhà phát triển sao chép, thực hiện các thay đổi, và sau đó cố gắng đẩy chúng lên trong khi một nhà phát triển khác đã đẩy trong thời gian đó, máy chủ sẽ từ chối các thay đổi của nhà phát triển đó. Họ sẽ được thông báo rằng họ đang cố gắng đẩy các thay đổi không phải là tua đi nhanh và họ sẽ phải kéo và hợp nhất trước khi có thể đẩy.
Quy trình làm việc này hấp dẫn đối với rất nhiều người vì nó là một mô hình mà nhiều người quen thuộc và thoải mái. Đây không phải là quy trình làm việc được sắp xếp hợp lý nhất mà Git có thể hỗ trợ, nhưng thường thì nó có thể là hiệu quả nhất.
Quy trình làm việc của Người quản lý Tích hợp
Bởi vì Git cho phép bạn có nhiều kho lưu trữ từ xa, có thể có một quy trình làm việc trong đó mỗi nhà phát triển có quyền ghi vào kho lưu trữ công khai của riêng họ và quyền đọc vào của mọi người khác. Kịch bản này thường bao gồm một kho lưu trữ chính tắc đại diện cho dự án “chính thức”. Để đóng góp vào dự án đó, bạn tạo bản sao công khai của riêng mình của dự án và đẩy các thay đổi của bạn vào đó. Sau đó, bạn có thể gửi một yêu cầu đến người bảo trì của dự án chính để kéo các thay đổi của bạn vào. Người bảo trì sau đó có thể thêm kho lưu trữ của bạn làm một điều khiển từ xa, kiểm tra các thay đổi của bạn cục bộ, hợp nhất chúng vào nhánh của họ, và đẩy trở lại kho lưu trữ của họ. Quá trình hoạt động như sau (xem Quy trình làm việc của người quản lý tích hợp):
-
Người bảo trì dự án đẩy lên kho lưu trữ công khai của họ.
-
Một người đóng góp sao chép kho lưu trữ đó và thực hiện các thay đổi.
-
Người đóng góp đẩy lên bản sao công khai của riêng họ.
-
Người đóng góp gửi cho người bảo trì một email yêu cầu họ kéo các thay đổi.
-
Người bảo trì thêm repo của người đóng góp làm một điều khiển từ xa và hợp nhất cục bộ.
-
Người bảo trì đẩy các thay đổi đã hợp nhất lên kho lưu trữ chính.
Đây là một quy trình làm việc rất phổ biến với các trang web như GitHub, nơi dễ dàng phân nhánh một dự án và đẩy các thay đổi của bạn vào nhánh của bạn để người bảo trì xem. Một trong những lợi thế chính của phương pháp này là bạn có thể tiếp tục làm việc, và người bảo trì của kho lưu trữ chính có thể kéo các thay đổi của bạn vào bất cứ lúc nào. Người đóng góp không phải chờ đợi dự án kết hợp các thay đổi của họ — mỗi bên có thể làm việc theo tốc độ của riêng mình.
Quy trình làm việc của Độc tài và Trung úy
Đây là một biến thể của một quy trình làm việc nhiều kho lưu trữ. Nó thường được sử dụng bởi các dự án khổng lồ với hàng trăm cộng tác viên; một ví dụ nổi tiếng là nhân Linux. Nhiều người quản lý tích hợp phụ trách các phần nhất định của kho lưu trữ; họ được gọi là "trung úy". Tất cả các trung úy đều có một người quản lý tích hợp được gọi là "nhà độc tài nhân từ". Kho lưu trữ của nhà độc tài nhân từ đóng vai trò là kho lưu trữ tham chiếu mà tất cả các cộng tác viên cần kéo từ đó. Quá trình hoạt động như thế này (xem Quy trình làm việc của nhà độc tài nhân từ):
-
Các nhà phát triển thông thường làm việc trên nhánh chủ đề của họ và rebase công việc của họ trên đỉnh của
master. Nhánhmasterlà của nhà độc tài. -
Các trung úy hợp nhất các nhánh chủ đề của các nhà phát triển vào nhánh
mastercủa họ. -
Nhà độc tài hợp nhất các nhánh
mastercủa các trung úy vào nhánhmastercủa nhà độc tài. -
Nhà độc tài đẩy
mastercủa họ lên kho lưu trữ tham chiếu để các nhà phát triển khác có thể rebase trên đó.
Loại quy trình làm việc này không phổ biến nhưng có thể hữu ích trong các dự án rất lớn hoặc trong các môi trường có thứ bậc cao, vì nó cho phép người lãnh đạo dự án (nhà độc tài) ủy thác nhiều công việc và thu thập các tập hợp mã lớn để tích hợp.
Đây là một số quy trình làm việc thường được sử dụng có thể có với một hệ thống phân tán như Git, nhưng bạn có thể thấy rằng nhiều biến thể có thể có để phù hợp với quy trình làm việc thực tế cụ thể của bạn. Bây giờ bạn có thể (chúng tôi hy vọng) xác định quy trình làm việc nào có thể phù hợp với bạn, chúng tôi sẽ đề cập đến một số ví dụ cụ thể hơn về cách hoàn thành các vai trò chính tạo nên các luồng khác nhau. Trong phần tiếp theo, bạn sẽ tìm hiểu về một vài trong số các mô hình phổ biến nhất để đóng góp vào một dự án.
Đóng góp vào một Dự án
Khó khăn chính khi mô tả cách đóng góp vào một dự án là có vô số biến thể về cách thực hiện điều đó. Bởi vì Git rất linh hoạt, mọi người có thể và thực sự làm việc cùng nhau theo nhiều cách, và thật khó để mô tả cách bạn nên đóng góp — mỗi dự án đều có một chút khác biệt. Một số biến số liên quan là số lượng người đóng góp tích cực, quy trình làm việc được chọn, quyền truy cập cam kết của bạn và có thể là phương pháp đóng góp bên ngoài.
Biến số đầu tiên là số lượng người đóng góp tích cực — có bao nhiêu người dùng đang tích cực đóng góp mã cho dự án này, và tần suất như thế nào? Trong nhiều trường hợp, bạn sẽ có hai hoặc ba nhà phát triển với một vài cam kết mỗi ngày, hoặc có thể ít hơn đối với các dự án hơi im lìm. Đối với các công ty hoặc dự án lớn hơn, số lượng nhà phát triển có thể lên tới hàng nghìn, với hàng trăm hoặc hàng nghìn cam kết đến mỗi ngày. Điều này quan trọng bởi vì với ngày càng nhiều nhà phát triển, bạn gặp phải nhiều vấn đề hơn trong việc đảm bảo mã của bạn áp dụng sạch sẽ hoặc có thể dễ dàng hợp nhất. Các thay đổi bạn gửi có thể trở nên lỗi thời hoặc bị hỏng nghiêm trọng bởi công việc được hợp nhất trong khi bạn đang làm việc hoặc trong khi các thay đổi của bạn đang chờ được phê duyệt hoặc áp dụng. Làm thế nào bạn có thể giữ cho mã của mình luôn cập nhật và các cam kết của bạn hợp lệ?
Biến số tiếp theo là quy trình làm việc được sử dụng cho dự án. Nó có phải là tập trung, với mỗi nhà phát triển có quyền ghi ngang nhau vào dòng mã chính không? Dự án có người bảo trì hoặc người quản lý tích hợp kiểm tra tất cả các bản vá không? Tất cả các bản vá có được bình duyệt và phê duyệt không? Bạn có tham gia vào quá trình đó không? Có hệ thống cấp phó (lieutenant) nào được áp dụng không, và bạn có phải gửi công việc của mình cho họ trước không?
Biến số tiếp theo là quyền truy cập cam kết của bạn. Quy trình làm việc cần thiết để đóng góp vào một dự án khác nhau nhiều nếu bạn có quyền ghi vào dự án so với nếu bạn không có. Nếu bạn không có quyền ghi, dự án thích chấp nhận công việc đóng góp như thế nào? Nó thậm chí có chính sách không? Bạn đang đóng góp bao nhiêu công việc cùng một lúc? Bạn đóng góp thường xuyên như thế nào?
Tất cả những câu hỏi này có thể ảnh hưởng đến cách bạn đóng góp hiệu quả cho một dự án và quy trình làm việc nào được ưu tiên hoặc có sẵn cho bạn. Chúng tôi sẽ đề cập đến các khía cạnh của từng vấn đề này trong một loạt các trường hợp sử dụng, chuyển từ đơn giản đến phức tạp hơn; bạn sẽ có thể xây dựng các quy trình làm việc cụ thể mà bạn cần trong thực tế từ các ví dụ này.
Hướng dẫn Cam kết (Commit Guidelines)
Trước khi chúng ta bắt đầu xem xét các trường hợp sử dụng cụ thể, đây là một lưu ý nhanh về thông báo cam kết.
Có một hướng dẫn tốt để tạo các cam kết và tuân thủ nó làm cho việc làm việc với Git và cộng tác với những người khác dễ dàng hơn nhiều.
Dự án Git cung cấp một tài liệu đưa ra một số mẹo hay để tạo các cam kết để gửi các bản vá — bạn có thể đọc nó trong mã nguồn Git trong tệp Documentation/SubmittingPatches.
Đầu tiên, các bài nộp của bạn không được chứa bất kỳ lỗi khoảng trắng nào.
Git cung cấp một cách dễ dàng để kiểm tra điều này — trước khi bạn cam kết, hãy chạy git diff --check, lệnh này xác định các lỗi khoảng trắng có thể có và liệt kê chúng cho bạn.
git diff --checkNếu bạn chạy lệnh đó trước khi cam kết, bạn có thể biết liệu bạn có sắp cam kết các vấn đề về khoảng trắng có thể gây khó chịu cho các nhà phát triển khác hay không.
Tiếp theo, hãy cố gắng làm cho mỗi cam kết trở thành một bộ thay đổi (changeset) tách biệt về mặt logic.
Nếu có thể, hãy cố gắng làm cho các thay đổi của bạn dễ hiểu — đừng viết mã cho cả cuối tuần về năm vấn đề khác nhau và sau đó gửi tất cả chúng dưới dạng một cam kết lớn vào thứ Hai.
Ngay cả khi bạn không cam kết trong cuối tuần, hãy sử dụng khu vực tổ chức (staging area) vào thứ Hai để chia công việc của bạn thành ít nhất một cam kết cho mỗi vấn đề, với một thông báo hữu ích cho mỗi cam kết.
Nếu một số thay đổi sửa đổi cùng một tệp, hãy thử sử dụng git add --patch để tổ chức một phần các tệp (được đề cập chi tiết trong [_interactive_staging]).
Ảnh chụp nhanh dự án ở đầu nhánh là giống hệt nhau cho dù bạn thực hiện một cam kết hay năm cam kết, miễn là tất cả các thay đổi được thêm vào tại một thời điểm nào đó, vì vậy hãy cố gắng làm cho mọi thứ dễ dàng hơn cho các nhà phát triển đồng nghiệp của bạn khi họ phải xem xét các thay đổi của bạn.
Cách tiếp cận này cũng giúp dễ dàng hơn trong việc lấy ra hoặc hoàn tác một trong các bộ thay đổi nếu bạn cần sau này. [_rewriting_history] mô tả một số thủ thuật Git hữu ích để viết lại lịch sử và tổ chức tệp tương tác — hãy sử dụng các công cụ này để giúp tạo ra một lịch sử sạch sẽ và dễ hiểu trước khi gửi công việc cho người khác.
Điều cuối cùng cần ghi nhớ là thông báo cam kết. Tạo thói quen tạo các thông báo cam kết chất lượng giúp việc sử dụng và cộng tác với Git dễ dàng hơn nhiều. Theo quy tắc chung, các thông báo của bạn nên bắt đầu bằng một dòng đơn không quá khoảng 50 ký tự và mô tả bộ thay đổi một cách ngắn gọn, tiếp theo là một dòng trống, tiếp theo là một lời giải thích chi tiết hơn. Dự án Git yêu cầu lời giải thích chi tiết hơn bao gồm động lực của bạn cho sự thay đổi và đối chiếu việc thực hiện nó với hành vi trước đó — đây là một hướng dẫn tốt để làm theo. Viết thông báo cam kết của bạn ở thể mệnh lệnh: "Fix bug" (Sửa lỗi) và không phải "Fixed bug" (Đã sửa lỗi) hoặc "Fixes bug" (Sửa lỗi). Dưới đây là một mẫu bạn có thể làm theo, chúng tôi đã điều chỉnh nhẹ từ một mẫu được viết ban đầu bởi Tim Pope:
Capitalized, short (50 chars or less) summary
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body. The blank
line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase will confuse you if you run the
two together.
Write your commit message in the imperative: "Fix bug" and not "Fixed bug"
or "Fixes bug." This convention matches up with commit messages generated
by commands like git merge and git revert.
Further paragraphs come after blank lines.
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, followed by a
single space, with blank lines in between, but conventions vary here
- Use a hanging indent
Nếu tất cả các thông báo cam kết của bạn tuân theo mô hình này, mọi thứ sẽ dễ dàng hơn nhiều cho bạn và các nhà phát triển mà bạn cộng tác.
Dự án Git có các thông báo cam kết được định dạng tốt — hãy thử chạy git log --no-merges ở đó để xem lịch sử cam kết dự án được định dạng đẹp mắt trông như thế nào.
|
Làm như chúng tôi nói, không phải như chúng tôi làm.
Vì mục đích ngắn gọn, nhiều ví dụ trong cuốn sách này không có thông báo cam kết được định dạng đẹp mắt như thế này; thay vào đó, chúng tôi chỉ đơn giản sử dụng tùy chọn Tóm lại, hãy làm như chúng tôi nói, không phải như chúng tôi làm. |
Nhóm Nhỏ Riêng tư (Private Small Team)
Thiết lập đơn giản nhất mà bạn có thể gặp phải là một dự án riêng tư với một hoặc hai nhà phát triển khác. “Riêng tư,” trong bối cảnh này, có nghĩa là nguồn đóng — không thể truy cập được đối với thế giới bên ngoài. Bạn và các nhà phát triển khác đều có quyền đẩy (push) vào kho lưu trữ.
Trong môi trường này, bạn có thể tuân theo quy trình làm việc tương tự như những gì bạn có thể làm khi sử dụng Subversion hoặc một hệ thống tập trung khác.
Bạn vẫn nhận được những lợi thế của những thứ như cam kết ngoại tuyến và phân nhánh và hợp nhất đơn giản hơn nhiều, nhưng quy trình làm việc có thể rất giống nhau; sự khác biệt chính là việc hợp nhất diễn ra ở phía máy khách thay vì trên máy chủ tại thời điểm cam kết.
Hãy xem nó có thể trông như thế nào khi hai nhà phát triển bắt đầu làm việc cùng nhau với một kho lưu trữ được chia sẻ.
Nhà phát triển đầu tiên, John, sao chép kho lưu trữ, thực hiện thay đổi và cam kết cục bộ.
Các thông báo giao thức đã được thay thế bằng … trong các ví dụ này để rút ngắn chúng đôi chút.
# John's Machine
$ git clone john@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim lib/simplegit.rb
$ git commit -am 'Remove invalid default value'
[master 738ee87] Remove invalid default value
1 files changed, 1 insertions(+), 1 deletions(-)
Nhà phát triển thứ hai, Jessica, làm điều tương tự — sao chép kho lưu trữ và cam kết một thay đổi:
# Jessica's Machine
$ git clone jessica@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim TODO
$ git commit -am 'Add reset task'
[master fbff5bc] Add reset task
1 files changed, 1 insertions(+), 0 deletions(-)
Bây giờ, Jessica đẩy công việc của cô ấy lên máy chủ, điều này hoạt động tốt:
# Jessica's Machine
$ git push origin master
...
To jessica@githost:simplegit.git
1edee6b..fbff5bc master -> master
Dòng cuối cùng của đầu ra ở trên hiển thị một thông báo trả về hữu ích từ thao tác đẩy.
Định dạng cơ bản là <oldref>..<newref> fromref → toref, trong đó oldref có nghĩa là tham chiếu cũ, newref có nghĩa là tham chiếu mới, fromref là tên của tham chiếu cục bộ đang được đẩy, và toref là tên của tham chiếu từ xa đang được cập nhật.
Bạn sẽ thấy đầu ra tương tự như thế này bên dưới trong các cuộc thảo luận, vì vậy có một ý tưởng cơ bản về ý nghĩa sẽ giúp hiểu các trạng thái khác nhau của các kho lưu trữ.
Thông tin chi tiết có sẵn trong tài liệu cho git-push.
Tiếp tục với ví dụ này, ngay sau đó, John thực hiện một số thay đổi, cam kết chúng vào kho lưu trữ cục bộ của mình và cố gắng đẩy chúng lên cùng một máy chủ:
# John's Machine
$ git push origin master
To john@githost:simplegit.git
! [rejected] master -> master (non-fast forward)
error: failed to push some refs to 'john@githost:simplegit.git'
Trong trường hợp này, việc đẩy của John thất bại vì việc đẩy các thay đổi của cô ấy trước đó của Jessica. Điều này đặc biệt quan trọng cần hiểu nếu bạn đã quen với Subversion, bởi vì bạn sẽ nhận thấy rằng hai nhà phát triển không chỉnh sửa cùng một tệp. Mặc dù Subversion tự động thực hiện hợp nhất như vậy trên máy chủ nếu các tệp khác nhau được chỉnh sửa, với Git, bạn phải trước tiên hợp nhất các cam kết cục bộ. Nói cách khác, John trước tiên phải tìm nạp (fetch) các thay đổi ngược dòng của Jessica và hợp nhất chúng vào kho lưu trữ cục bộ của mình trước khi anh ta được phép đẩy.
Là bước đầu tiên, John tìm nạp công việc của Jessica (điều này chỉ tìm nạp công việc ngược dòng của Jessica, nó chưa hợp nhất nó vào công việc của John):
$ git fetch origin
...
From john@githost:simplegit
+ 049d078...fbff5bc master -> origin/master
Tại thời điểm này, kho lưu trữ cục bộ của John trông giống như thế này:
Bây giờ John có thể hợp nhất công việc của Jessica mà anh ấy đã tìm nạp vào công việc cục bộ của chính mình:
$ git merge origin/master
Merge made by the 'recursive' strategy.
TODO | 1 +
(((git commands, request-pull)))
Once your work has been pushed to your fork of the repository, you need to notify the maintainers of the original project that you have work you'd like them to merge.
This is often called a _pull request_, and you typically generate such a request either via the website -- GitHub has its own "`Pull Request`" mechanism that we'll go over in <<ch06-github#ch06-github>> -- or you can run the `git request-pull` command and email the subsequent output to the project maintainer manually.
The `git request-pull` command takes the base branch into which you want your topic branch pulled and the Git repository URL you want them to pull from, and produces a summary of all the changes you're asking to be pulled.
For instance, if Jessica wants to send John a pull request, and she's done two commits on the topic branch she just pushed, she can run this:
[source,console]
$ git request-pull origin/master myfork The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40: Jessica Smith (1): Create new function
are available in the git repository at:
https://githost/simplegit.git featureA
Jessica Smith (2): Add limit to log function Increase log output to 30 from 25
lib/simplegit.rb | 10 +++++++++- 1 files changed, 9 insertions(+), 1 deletions(-)
This output can be sent to the maintainer -- it tells them where the work was branched from, summarizes the commits, and identifies from where the new work is to be pulled. On a project for which you're not the maintainer, it's generally easier to have a branch like `master` always track `origin/master` and to do your work in topic branches that you can easily discard if they're rejected. Having work themes isolated into topic branches also makes it easier for you to rebase your work if the tip of the main repository has moved in the meantime and your commits no longer apply cleanly. For example, if you want to submit a second topic of work to the project, don't continue working on the topic branch you just pushed up -- start over from the main repository's `master` branch: [source,console]
$ git checkout -b featureB origin/master … work … $ git commit $ git push myfork featureB $ git request-pull origin/master myfork … email generated request pull to maintainer … $ git fetch origin
Now, each of your topics is contained within a silo -- similar to a patch queue -- that you can rewrite, rebase, and modify without the topics interfering or interdepending on each other, like so: .Initial commit history with `featureB` work image::images/public-small-1.png[Initial commit history with `featureB` work] Let's say the project maintainer has pulled in a bunch of other patches and tried your first branch, but it no longer cleanly merges. In this case, you can try to rebase that branch on top of `origin/master`, resolve the conflicts for the maintainer, and then resubmit your changes: [source,console]
$ git checkout featureA $ git rebase origin/master $ git push -f myfork featureA
This rewrites your history to now look like <<psp_b>>. [[psp_b]] .Commit history after `featureA` work image::images/public-small-2.png[Commit history after `featureA` work] Because you rebased the branch, you have to specify the `-f` to your push command in order to be able to replace the `featureA` branch on the server with a commit that isn't a descendant of it. An alternative would be to push this new work to a different branch on the server (perhaps called `featureAv2`). Let's look at one more possible scenario: the maintainer has looked at work in your second branch and likes the concept but would like you to change an implementation detail. You'll also take this opportunity to move the work to be based off the project's current `master` branch. You start a new branch based off the current `origin/master` branch, squash the `featureB` changes there, resolve any conflicts, make the implementation change, and then push that as a new branch: (((git commands, merge, squash))) [source,console]
$ git checkout -b featureBv2 origin/master $ git merge --squash featureB … change implementation … $ git commit $ git push myfork featureBv2
The `--squash` option takes all the work on the merged branch and squashes it into one changeset producing the repository state as if a real merge happened, without actually making a merge commit. This means your future commit will have one parent only and allows you to introduce all the changes from another branch and then make more changes before recording the new commit. Also the `--no-commit` option can be useful to delay the merge commit in case of the default merge process. At this point, you can notify the maintainer that you've made the requested changes, and that they can find those changes in your `featureBv2` branch. .Commit history after `featureBv2` work image::images/public-small-3.png[Commit history after `featureBv2` work] [[_project_over_email]] ==== Public Project over Email (((contributing, public large project))) Many projects have established procedures for accepting patches -- you'll need to check the specific rules for each project, because they will differ. Since there are several older, larger projects which accept patches via a developer mailing list, we'll go over an example of that now. The workflow is similar to the previous use case -- you create topic branches for each patch series you work on. The difference is how you submit them to the project. Instead of forking the project and pushing to your own writable version, you generate email versions of each commit series and email them to the developer mailing list: [source,console]
$ git checkout -b topicA … work … $ git commit … work … $ git commit
(((git commands, format-patch))) Now you have two commits that you want to send to the mailing list. You use `git format-patch` to generate the mbox-formatted files that you can email to the list -- it turns each commit into an email message with the first line of the commit message as the subject and the rest of the message plus the patch that the commit introduces as the body. The nice thing about this is that applying a patch from an email generated with `format-patch` preserves all the commit information properly. [source,console]
$ git format-patch -M origin/master 0001-add-limit-to-log-function.patch 0002-increase-log-output-to-30-from-25.patch
The `format-patch` command prints out the names of the patch files it creates. The `-M` switch tells Git to look for renames. The files end up looking like this: [source,console]
$ cat 0001-add-limit-to-log-function.patch From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001 From: Jessica Smith <jessica@example.com> Date: Sun, 6 Apr 2008 10:17:23 -0700 Subject: [PATCH 1/2] Add limit to log function
Limit log functionality to the first 20
lib/simplegit.rb | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-)
diff --git a/lib/simplegit.rb b/lib/simplegit.rb index 76f47bc..f9815f1 100644 --- a/lib/simplegit.rb + b/lib/simplegit.rb @@ -14,7 +14,7 @@ class SimpleGit end
def log(treeish = 'master')
- command("git log #{treeish}")
+ command("git log -n 20 #{treeish}")
end
def ls_tree(treeish = 'master')
2.1.0
You can also edit these patch files to add more information for the email list that you don't want to show up in the commit message. If you add text between the `---` line and the beginning of the patch (the `diff --git` line), the developers can read it, but that content is ignored by the patching process. To email this to a mailing list, you can either paste the file into your email program or send it via a command-line program. Pasting the text often causes formatting issues, especially with "`smarter`" clients that don't preserve newlines and other whitespace appropriately. Luckily, Git provides a tool to help you send properly formatted patches via IMAP, which may be easier for you. We'll demonstrate how to send a patch via Gmail, which happens to be the email agent we know best; you can read detailed instructions for a number of mail programs at the end of the aforementioned `Documentation/SubmittingPatches` file in the Git source code. (((git commands, config)))(((email))) First, you need to set up the imap section in your `~/.gitconfig` file. You can set each value separately with a series of `git config` commands, or you can add them manually, but in the end your config file should look something like this: [source,ini]
folder = "[Gmail]/Drafts" host = imaps://imap.gmail.com user = user@gmail.com pass = YX]8g76G_2^sFbd port = 993 sslverify = false
If your IMAP server doesn't use SSL, the last two lines probably aren't necessary, and the host value will be `imap://` instead of `imaps://`. When that is set up, you can use `git imap-send` to place the patch series in the Drafts folder of the specified IMAP server: [source,console]
$ cat *.patch |git imap-send Resolving imap.gmail.com… ok Connecting to [74.125.142.109]:993… ok Logging in… sending 2 messages 100% (2/2) done
At this point, you should be able to go to your Drafts folder, change the To field to the mailing list you're sending the patch to, possibly CC the maintainer or person responsible for that section, and send it off. Bạn cũng có thể gửi các bản vá thông qua một máy chủ SMTP. Như trước đây, bạn có thể đặt từng giá trị riêng biệt với một loạt các lệnh `git config`, hoặc bạn có thể thêm chúng thủ công trong phần sendemail trong tệp `~/.gitconfig` của bạn: [source,ini]
smtpencryption = tls smtpserver = smtp.gmail.com smtpuser = user@gmail.com smtpserverport = 587
Sau khi thực hiện xong, bạn có thể sử dụng `git send-email` để gửi các bản vá của mình: [source,console]
$ git send-email *.patch 0001-add-limit-to-log-function.patch 0002-increase-log-output-to-30-from-25.patch Who should the emails appear to be from? [Jessica Smith <jessica@example.com>] Emails will be sent from: Jessica Smith <jessica@example.com> Who should the emails be sent to? jessica@example.com Message-ID to be used as In-Reply-To for the first email? y
Sau đó, Git nhổ ra một loạt thông tin nhật ký trông giống như thế này cho mỗi bản vá bạn đang gửi: [source,text]
(mbox) Adding cc: Jessica Smith <jessica@example.com> from \line 'From: Jessica Smith <jessica@example.com>' OK. Log says: Sendmail: /usr/sbin/sendmail -i jessica@example.com From: Jessica Smith <jessica@example.com> To: jessica@example.com Subject: [PATCH 1/2] Add limit to log function Date: Sat, 30 May 2009 13:29:15 -0700 Message-Id: <1243715356-61726-1-git-send-email-jessica@example.com> X-Mailer: git-send-email 1.6.2.rc1.20.g8c5b.dirty In-Reply-To: <y> References: <y>
Result: OK
[TIP] ==== Để được trợ giúp về cấu hình hệ thống và email của bạn, thêm các mẹo và thủ thuật, và một hộp cát để gửi bản vá thử nghiệm qua email, hãy truy cập https://git-send-email.io[git-send-email.io^]. ==== ==== Tóm tắt Trong phần này, chúng tôi đã đề cập đến nhiều quy trình làm việc, và nói về sự khác biệt giữa làm việc như một phần của một nhóm nhỏ trong các dự án nguồn đóng so với đóng góp cho một dự án công khai lớn. Bạn biết kiểm tra các lỗi khoảng trắng trước khi cam kết, và có thể viết một thông báo cam kết tuyệt vời. Bạn đã học cách định dạng các bản vá, và gửi email chúng đến danh sách gửi thư của nhà phát triển. Việc xử lý các hợp nhất cũng đã được đề cập trong bối cảnh của các quy trình làm việc khác nhau. Bây giờ bạn đã chuẩn bị tốt để cộng tác trên bất kỳ dự án nào. Tiếp theo, bạn sẽ thấy cách làm việc ở mặt bên kia của đồng xu: duy trì một dự án Git. Bạn sẽ học cách trở thành một nhà độc tài nhân từ hoặc người quản lý tích hợp. [[_maintaining_project]] === Duy trì một Dự án Ngoài việc biết cách đóng góp hiệu quả vào một dự án, bạn có thể sẽ cần biết cách duy trì một dự án. Điều này có thể bao gồm việc chấp nhận và áp dụng các bản vá được tạo qua `format-patch` và được gửi qua email cho bạn, hoặc tích hợp các thay đổi trong các nhánh từ xa cho các kho lưu trữ bạn đã thêm làm điều khiển từ xa. Cho dù bạn duy trì một kho lưu trữ chính tắc hay muốn giúp đỡ bằng cách xác minh hoặc phê duyệt các bản vá, bạn cần biết cách chấp nhận công việc theo cách rõ ràng nhất cho những người đóng góp khác và bền vững cho bạn về lâu dài. ==== Làm việc trong các Nhánh Chủ đề (((maintaining, topic branches))) Khi bạn đang nghĩ đến việc tích hợp công việc mới, một ý tưởng hay là thử nó trong một nhánh chủ đề -- một nhánh tạm thời được tạo riêng để thử nghiệm công việc mới đó. Bằng cách này, thật dễ dàng để tinh chỉnh một bản vá một cách riêng lẻ và để nó lại nếu nó không hoạt động cho đến khi bạn có thời gian để quay lại với nó. Nếu bạn tạo một tên nhánh đơn giản dựa trên chủ đề của công việc bạn sẽ thử, chẳng hạn như `ruby_client` hoặc một cái gì đó tương tự mô tả, bạn có thể dễ dàng nhớ nó nếu bạn phải từ bỏ nó một thời gian và quay lại sau. Người bảo trì của dự án Git cũng có xu hướng đặt không gian tên cho các nhánh này -- chẳng hạn như `sc/ruby_client`, trong đó `sc` là viết tắt của người đang đóng góp công việc. Như bạn sẽ nhớ, bạn có thể tạo nhánh dựa trên nhánh `master` của mình như thế này: [source,console]
$ git branch sc/ruby_client master
Hoặc, nếu bạn cũng muốn chuyển sang nó ngay lập tức, bạn có thể sử dụng lệnh `checkout -b`: [source,console]
$ git checkout -b sc/ruby_client master
Bây giờ bạn đã sẵn sàng để thêm công việc được đóng góp vào nhánh chủ đề này và xác định xem bạn có muốn hợp nhất nó vào các nhánh dài hạn của mình hay không. [[_applying_patches_from_email]] ==== Áp dụng các Bản vá từ Email (((patches, applying from email))) Nếu bạn nhận được một bản vá qua email mà bạn cần tích hợp vào dự án của mình, bạn cần áp dụng bản vá trong nhánh chủ đề của mình để đánh giá nó. Có hai cách để áp dụng một bản vá được gửi qua email: với `git apply` hoặc với `git am`. ===== Áp dụng một Bản vá với `apply` Nếu bạn nhận được bản vá từ một người đã tạo nó bằng lệnh `git diff` hoặc một lệnh `diff` của Unix, bạn có thể áp dụng nó bằng lệnh `git apply`. Giả sử bạn đã lưu bản vá tại `/tmp/patch-ruby-client.patch`, bạn có thể áp dụng bản vá như thế này: [source,console]
$ git apply /tmp/patch-ruby-client.patch
Điều này sửa đổi các tệp trong thư mục làm việc của bạn. Nó gần như giống hệt với việc chạy lệnh `patch -p1` để áp dụng bản vá, nhưng nó thận trọng hơn và chấp nhận ít kết quả khớp mờ hơn so với patch. Nó cũng xử lý việc thêm, xóa và đổi tên tệp nếu chúng được mô tả ở định dạng `git diff`, điều mà `patch` sẽ không làm. Cuối cùng, `git apply` là một mô hình "áp dụng tất cả hoặc hủy bỏ tất cả" trong đó hoặc mọi thứ được áp dụng hoặc không có gì cả, trong khi `patch` có thể áp dụng một phần các tệp vá, để lại thư mục làm việc của bạn ở trạng thái kỳ lạ. `git apply` nói chung là thận trọng hơn nhiều so với `patch`. Nó sẽ không tạo ra một cam kết cho bạn -- sau khi chạy nó, bạn phải tự tổ chức và cam kết các thay đổi được giới thiệu. Bạn cũng có thể sử dụng `git apply` để xem liệu một bản vá có được áp dụng sạch sẽ hay không trước khi bạn thực sự cố gắng áp dụng nó -- bạn có thể chạy `git apply --check` với bản vá: [source,console]
$ git apply --check 0001-see-if-this-helps-the-gem.patch error: patch failed: lib/simplegit.rb:29 error: lib/simplegit.rb: patch does not apply
Nếu không có đầu ra, thì bản vá sẽ được áp dụng sạch sẽ. Lệnh này cũng thoát với trạng thái khác không nếu kiểm tra không thành công, vì vậy bạn có thể sử dụng nó trong các tập lệnh nếu bạn muốn. ===== Áp dụng một Bản vá với `am` (((git commands, am))) Nếu người đóng góp là người dùng Git và đủ tốt để sử dụng lệnh `format-patch` để tạo bản vá của họ, thì công việc của bạn sẽ dễ dàng hơn vì bản vá chứa thông tin tác giả và thông điệp cam kết. Nếu có thể, hãy khuyến khích những người đóng góp của bạn sử dụng `format-patch` thay vì `diff` để tạo các bản vá cho bạn. Bạn chỉ nên sử dụng `git apply` cho các bản vá cũ và những thứ tương tự. Để áp dụng một bản vá được tạo bởi `format-patch`, bạn sử dụng `git am` (lệnh được đặt tên là `am` vì nó được sử dụng để "áp dụng một loạt các bản vá từ một hộp thư"). Về mặt kỹ thuật, `git am` được xây dựng để đọc một tệp mbox, đây là một định dạng văn bản thuần túy, đơn giản để lưu trữ một hoặc nhiều thư email trong một tệp văn bản. Nó trông giống như thế này: [source]
From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001 From: Jessica Smith <jessica@example.com> Date: Sun, 6 Apr 2008 10:17:23 -0700 Subject: [PATCH 1/2] Add limit to log function
Limit log functionality to the first 20
Đây là phần đầu của đầu ra của lệnh `format-patch` mà bạn đã thấy trong phần trước; nó cũng tạo thành một định dạng email mbox hợp lệ. Nếu ai đó đã gửi email cho bạn bản vá đúng cách bằng `git send-email`, và bạn tải xuống nó ở định dạng mbox, thì bạn có thể trỏ `git am` vào tệp mbox đó, và nó sẽ bắt đầu áp dụng tất cả các bản vá mà nó thấy. Nếu bạn chạy một ứng dụng thư khách có thể lưu nhiều email ra ở định dạng mbox, bạn có thể lưu toàn bộ một loạt các bản vá vào một tệp và sau đó sử dụng `git am` để áp dụng tất cả chúng cùng một lúc. Tuy nhiên, nếu ai đó đã tải lên một tệp vá được tạo qua `format-patch` lên một hệ thống vé hoặc một cái gì đó, bạn có thể lưu tệp cục bộ và sau đó chuyển tệp đó cho `git am` để áp dụng nó: [source,console]
$ git am 0001-limit-log-function.patch Applying: Add limit to log function
Bạn có thể thấy rằng nó đã được áp dụng sạch sẽ và tự động tạo ra cam kết mới cho bạn. Thông tin tác giả được lấy từ các tiêu đề `From` và `Date` của email, và thông điệp cho cam kết được lấy từ `Subject` và phần thân (trước bản vá) của email. Ví dụ, nếu bản vá này được áp dụng từ ví dụ mbox ở trên, cam kết được tạo sẽ trông giống như thế này: [source,console]
$ git log --pretty=fuller -1 commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0 Author: Jessica Smith <jessica@example.com> AuthorDate: Sun Apr 6 10:17:23 2008 -0700 Commit: Scott Chacon <schacon@gmail.com> CommitDate: Thu Apr 9 09:19:06 2009 -0700
Add limit to log function
Limit log functionality to the first 20
Thông tin `Commit` cho biết người đã áp dụng bản vá và thời gian nó được áp dụng. Thông tin `Author` là cá nhân đã tạo ra bản vá ban đầu và khi nó được tạo ra ban đầu. Nhưng có thể bản vá sẽ không được áp dụng sạch sẽ. Có lẽ nhánh chính của bạn đã phân kỳ quá xa so với nhánh mà bản vá được xây dựng từ đó, hoặc bản vá phụ thuộc vào một bản vá khác mà bạn chưa áp dụng. Trong trường hợp đó, quá trình `git am` sẽ thất bại và hỏi bạn muốn làm gì: [source,console]
$ git am 0001-see-if-this-helps-the-gem.patch Applying: See if this helps the gem error: patch failed: lib/simplegit.rb:29 error: lib/simplegit.rb: patch does not apply Patch failed at 0001. When you have resolved this problem, run "git am --continue". If you prefer to skip this patch, run "git am --skip". To restore the original branch and stop patching, run "git am --abort".
Lệnh này đặt các dấu xung đột trong bất kỳ tệp nào nó có vấn đề, giống như một thao tác `merge` hoặc `rebase` bị xung đột. Bạn giải quyết vấn đề này theo cách tương tự -- chỉnh sửa tệp để giải quyết xung đột, tổ chức tệp mới, và sau đó chạy `git am --continue` để tiếp tục với bản vá tiếp theo: [source,console]
$ (fix the file) $ git add . $ git am --continue Applying: See if this helps the gem
Nếu bạn muốn Git cố gắng giải quyết xung đột một cách thông minh hơn một chút, bạn có thể chuyển tùy chọn `-3` cho nó, điều này làm cho Git cố gắng hợp nhất ba chiều. Tùy chọn này không được bật theo mặc định vì nó không hoạt động nếu cam kết mà bản vá nói nó dựa trên không có trong kho lưu trữ của bạn. Nếu bạn có cam kết đó -- nếu bản vá dựa trên một cam kết công khai -- thì tùy chọn `-3` thường thông minh hơn nhiều về việc áp dụng một bản vá xung đột: [source,console]
$ git am -3 0001-see-if-this-helps-the-gem.patch Applying: See if this helps the gem error: patch failed: lib/simplegit.rb:29 error: lib/simplegit.rb: patch does not apply Using index info to reconstruct a base tree… Falling back to patching base and 3-way merge… No changes — Patch already applied.
Trong trường hợp này, chúng tôi đang cố gắng áp dụng một bản vá mà chúng tôi đã áp dụng. Nếu không có tùy chọn `-3`, nó sẽ trông giống như một xung đột. Nếu bạn đang áp dụng một số bản vá từ một mbox, bạn cũng có thể chạy lệnh `am` ở chế độ tương tác, nó dừng lại ở mỗi bản vá nó tìm thấy và hỏi bạn có muốn áp dụng nó không: [source,console]
$ git am -3 -i mbox Commit Body is:
See if this helps the gem
Apply? [y]es/[n]o/[q]uit/[a]ll:
Điều này rất hay nếu bạn có một số bản vá, bởi vì bạn có thể xem bản vá trước nếu bạn không nhớ nó là gì, hoặc không áp dụng bản vá nếu bạn đã làm như vậy. Khi tất cả các bản vá cho chủ đề của bạn được áp dụng và cam kết vào nhánh của bạn, bạn có thể chọn có tích hợp chúng vào một nhánh chạy dài hơn hay không và làm thế nào. ==== Kiểm tra các Nhánh Từ xa Nếu đóng góp của bạn đến từ một người dùng Git đã thiết lập kho lưu trữ của riêng họ, đã đẩy một số thay đổi vào đó, và sau đó gửi cho bạn URL đến kho lưu trữ và tên của nhánh từ xa chứa các thay đổi, bạn có thể thêm chúng làm một điều khiển từ xa và thực hiện hợp nhất cục bộ. Ví dụ, nếu Jessica gửi cho bạn một email nói rằng cô ấy có một tính năng mới tuyệt vời trong nhánh `ruby-client` của kho lưu trữ của cô ấy, bạn có thể kiểm tra nó bằng cách thêm kho lưu trữ làm một điều khiển từ xa và kiểm tra nhánh đó cục bộ: [source,console]
$ git remote add jessica https://github.com/jessica/myproject.git $ git fetch jessica $ git checkout -b rubyclient jessica/ruby-client
Nếu cô ấy gửi email cho bạn một lần nữa sau đó với một nhánh khác chứa một tính năng tuyệt vời khác, bạn có thể tìm nạp và kiểm tra nó vì bạn đã thiết lập điều khiển từ xa. Điều này hữu ích nhất nếu bạn đang làm việc với một người một cách nhất quán. Nếu ai đó chỉ có một bản vá duy nhất để đóng góp một lần, thì việc chấp nhận nó qua email có thể ít tốn thời gian hơn so với việc yêu cầu mọi người chạy máy chủ của riêng họ và phải liên tục thêm và xóa các điều khiển từ xa để nhận được một vài bản vá. Bạn cũng không có khả năng muốn có hàng trăm điều khiển từ xa cho những người chỉ đóng góp một hoặc hai bản vá. Tuy nhiên, các tập lệnh và các dịch vụ được lưu trữ có thể làm cho điều này dễ dàng hơn -- nó phụ thuộc phần lớn vào cách bạn phát triển và cách những người đóng góp của bạn làm. Một lợi thế khác của phương pháp này là bạn cũng có được lịch sử của các cam kết. Mặc dù bạn có thể có các vấn đề hợp pháp với các thông điệp cam kết, bạn nhận được công việc của họ trong lịch sử của bạn; đó là một hợp nhất ba chiều. Nếu bạn không làm việc với một người một cách nhất quán nhưng vẫn muốn kéo từ họ theo cách này, bạn có thể cung cấp URL của kho lưu trữ từ xa cho lệnh `git pull`. Điều này thực hiện một lần kéo duy nhất và sẽ không lưu URL làm một tham chiếu từ xa: [source,console]
$ git pull https://github.com/onetimeguy/project From https://github.com/onetimeguy/project * branch HEAD → FETCH_HEAD Merge made by the 'recursive' strategy. …
[[_determining_what_is_introduced]] ==== Xác định những gì được giới thiệu Bây giờ bạn có một nhánh chủ đề chứa công việc được đóng góp. Tại thời điểm này, bạn có thể xác định những gì bạn muốn làm với nó. Phần này xem xét lại một vài lệnh để bạn có thể thấy cách bạn có thể sử dụng chúng để xem xét chính xác những gì bạn sẽ giới thiệu nếu bạn hợp nhất điều này vào nhánh chính của mình. Thường thì một đánh giá tốt là xem tất cả các cam kết trong nhánh này không có trong nhánh `master` của bạn. Bạn có thể loại trừ các cam kết trong nhánh `master` bằng cách thêm tùy chọn `--not` trước tên nhánh. Điều này giống như định dạng `master..contrib` chúng ta đã sử dụng trước đó. Ví dụ, nếu người đóng góp của bạn gửi cho bạn hai bản vá và bạn tạo một nhánh có tên `contrib` và áp dụng các bản vá đó ở đó, bạn có thể chạy lệnh này: [source,console]
$ git log contrib --not master commit 5b6235bd297351589ff952d52b6de8858ab42d34 Author: Scott Chacon <schacon@gmail.com> Date: Fri Oct 24 09:53:59 2008 -0700
See if this helps the gem
commit 7482e0d16d04bea79d0dba898f36c4af2e6b52a5 Author: Scott Chacon <schacon@gmail.com> Date: Mon Oct 22 19:38:36 2008 -0700
Update gemspec to hopefully work better
Để xem những thay đổi mà mỗi cam kết giới thiệu, hãy nhớ rằng bạn có thể chuyển tùy chọn `-p` cho `git log` và nó sẽ nối diff của các thay đổi vào mỗi cam kết. Để xem một diff đầy đủ về những gì sẽ xảy ra nếu bạn hợp nhất nhánh chủ đề này với một nhánh khác, bạn có thể phải sử dụng một mẹo kỳ lạ để có được kết quả chính xác. Bạn có thể nghĩ đến việc chạy lệnh này: [source,console]
$ git diff master
Lệnh này là một diff, nhưng nó có thể gây hiểu lầm. Nếu nhánh `master` của bạn đã di chuyển về phía trước kể từ khi bạn tạo nhánh chủ đề từ nó, thì bạn sẽ nhận được các kết quả có vẻ kỳ lạ. Điều này xảy ra bởi vì Git so sánh trực tiếp các ảnh chụp nhanh của cam kết cuối cùng của nhánh chủ đề bạn đang ở và cam kết cuối cùng của nhánh `master`. Ví dụ, nếu bạn đã thêm một dòng vào một tệp trên nhánh `master`, một diff trực tiếp của nhánh chủ đề sẽ trông giống như bạn sẽ thêm dòng đó, trong khi thực tế bạn đang xóa nó trong nhánh chủ đề. Nếu `master` là một tổ tiên trực tiếp của nhánh chủ đề của bạn, đây không phải là vấn đề; nhưng nếu hai lịch sử đã phân kỳ, diff sẽ trông giống như bạn đang thêm tất cả những thứ mới trong nhánh chủ đề của bạn và xóa mọi thứ là duy nhất đối với nhánh `master`. Những gì bạn thực sự muốn thấy là những thay đổi được thêm vào trong nhánh chủ đề -- công việc bạn sẽ giới thiệu nếu bạn hợp nhất nhánh này với `master`. Bạn có được điều này bằng cách để Git so sánh cam kết cuối cùng trên nhánh chủ đề của bạn với tổ tiên chung đầu tiên của nó với nhánh `master`. Về mặt kỹ thuật, bạn có thể làm điều đó bằng cách tìm ra tổ tiên chung một cách rõ ràng và sau đó chạy `diff` của bạn trên đó: [source,console]
$ git merge-base contrib master 36c7dba2c95e6bbb78dfa822519ecfec6e1ca649 $ git diff 36c7dba
Tuy nhiên, điều đó không thuận tiện, vì vậy Git cung cấp một cách ngắn hơn để làm điều tương tự: cú pháp ba chấm. Với lệnh `diff`, bạn có thể đặt ba dấu chấm sau một nhánh khác để thực hiện một `diff` giữa cam kết cuối cùng của nhánh bạn đang ở và tổ tiên chung của nó với một nhánh khác: [source,console]
$ git diff master…contrib
Lệnh này chỉ cho bạn thấy công việc mà nhánh chủ đề hiện tại của bạn đã giới thiệu kể từ tổ tiên chung của nó với `master`. Đây là một cú pháp rất hữu ích cần nhớ. ==== Tích hợp Công việc được Đóng góp Khi tất cả công việc trong nhánh chủ đề của bạn đã sẵn sàng để được tích hợp vào một nhánh chính hơn, câu hỏi là làm thế nào để làm điều đó. Hơn nữa, bạn muốn sử dụng quy trình làm việc tổng thể nào để duy trì dự án của mình? Bạn có một số lựa chọn, vì vậy chúng tôi sẽ đề cập đến một vài trong số đó. ===== Quy trình làm việc Hợp nhất Một quy trình làm việc đơn giản là hợp nhất công việc của bạn vào nhánh `master` của bạn. Trong kịch bản này, bạn có một nhánh `master` chứa mã ổn định. Khi bạn có công việc trong một nhánh chủ đề mà bạn đã làm hoặc ai đó đã đóng góp và bạn đã xác minh, bạn hợp nhất nó vào nhánh `master` của mình, xóa nhánh chủ đề, và sau đó tiếp tục. Nếu chúng ta có một kho lưu trữ với công việc trong hai nhánh có tên `ruby_client` và `php_client` trông giống như <<merging_example>> và chúng ta hợp nhất `ruby_client` trước và sau đó `php_client` thứ hai, thì lịch sử của bạn sẽ kết thúc trông giống như <<merging_example_2>>. [[merging_example]] .Một lịch sử với nhiều nhánh chủ đề image::images/large-merges-1.png[Một lịch sử với nhiều nhánh chủ đề] [[merging_example_2]] .Sau khi hợp nhất nhánh chủ đề image::images/large-merges-2.png[Sau khi hợp nhất nhánh chủ đề] Đó có lẽ là quy trình làm việc đơn giản nhất, nhưng nó có thể có vấn đề nếu bạn đang xử lý các dự án lớn hơn hoặc ổn định hơn nơi bạn muốn cẩn thận hơn về những gì bạn giới thiệu. Nếu bạn có một dự án quan trọng hơn, bạn có thể muốn sử dụng một chu trình hợp nhất hai pha. Trong kịch bản này, bạn có hai nhánh chạy dài, `master` và `develop`, trong đó bạn xác định rằng `master` chỉ được cập nhật khi một bản phát hành rất ổn định được cắt và tất cả mã mới được tích hợp vào nhánh `develop`. Bạn thường xuyên đẩy cả hai nhánh này lên kho lưu trữ công khai. Mỗi khi bạn có một nhánh chủ đề mới để hợp nhất vào (<<merging_cycle_1>>), bạn hợp nhất nó vào `develop` (<<merging_cycle_2>>); sau đó, khi bạn gắn thẻ một bản phát hành, bạn tua nhanh `master` đến bất cứ đâu mà nhánh `develop` hiện đã ổn định (<<merging_cycle_3>>). [[merging_cycle_1]] .Trước khi hợp nhất nhánh chủ đề image::images/merging-cycle-1.png[Trước khi hợp nhất nhánh chủ đề] [[merging_cycle_2]] .Sau khi hợp nhất nhánh chủ đề image::images/merging-cycle-2.png[Sau khi hợp nhất nhánh chủ đề] [[merging_cycle_3]] .Sau khi phát hành dự án image::images/merging-cycle-3.png[Sau khi phát hành dự án] Bằng cách này, khi mọi người sao chép kho lưu trữ của dự án của bạn, họ có thể kiểm tra `master` để xây dựng phiên bản ổn định mới nhất và dễ dàng cập nhật trên đó, hoặc họ có thể kiểm tra `develop`, đó là nội dung tiên tiến hơn. Bạn cũng có thể mở rộng khái niệm này bằng cách có một nhánh `integrate` nơi tất cả công việc được hợp nhất lại với nhau. Sau đó, khi cơ sở mã trên nhánh đó ổn định và vượt qua các bài kiểm tra, bạn hợp nhất nó vào một nhánh `develop`; và khi điều đó đã chứng tỏ mình ổn định một thời gian, bạn tua nhanh nhánh `master` của mình. ===== Quy trình làm việc Hợp nhất Lớn Dự án Git có bốn nhánh chạy dài: `master`, `next`, và `seen` (trước đây là `pu` cho các bản cập nhật được đề xuất) cho công việc mới, và `maint` cho các bản vá bảo trì. Khi công việc mới được giới thiệu bởi những người đóng góp, nó được thu thập vào các nhánh chủ đề trong kho lưu trữ của người bảo trì theo cách tương tự như những gì chúng ta đã mô tả (xem <<large_merging_1>>). Tại thời điểm này, các chủ đề được đánh giá để xác định xem chúng có an toàn và sẵn sàng để sử dụng hay không hoặc liệu chúng có cần thêm công việc hay không. Nếu chúng an toàn, chúng được hợp nhất vào `next`, và nhánh đó được đẩy lên để mọi người có thể thử các chủ đề được tích hợp lại với nhau. [[large_merging_1]] .Quản lý một loạt các nhánh chủ đề được đóng góp song song phức tạp image::images/large-merging-1.png[Quản lý một loạt các nhánh chủ đề được đóng góp song song phức tạp] Nếu các chủ đề vẫn cần công việc, chúng được hợp nhất vào `seen` thay thế. Khi được xác định rằng chúng hoàn toàn ổn định, các chủ đề được hợp nhất lại vào `master`. Các nhánh `next` và `seen` sau đó được xây dựng lại từ nhánh `master`. Điều này thường có nghĩa là `next` được xây dựng lại bằng một rebase, và `seen` bằng một hợp nhất (xem <<large_merging_2>>). [[large_merging_2]] .Hợp nhất các nhánh chủ đề vào các nhánh tích hợp dài hạn image::images/large-merging-2.png[Hợp nhất các nhánh chủ đề vào các nhánh tích hợp dài hạn] Khi một nhánh chủ đề cuối cùng đã được hợp nhất vào `master`, nó sẽ bị xóa khỏi kho lưu trữ. ===== Quy trình làm việc Rebase và Cherry-Pick (((workflows, rebasing and cherry-picking))) Các người bảo trì khác thích rebase hoặc cherry-pick công việc được đóng góp trên đỉnh của nhánh `master` của họ, thay vì hợp nhất nó vào, để giữ một lịch sử gần như tuyến tính. Khi bạn có công việc trong một nhánh chủ đề và đã xác định rằng bạn muốn tích hợp nó, bạn di chuyển đến nhánh đó và chạy lệnh `rebase` để xây dựng lại các thay đổi trên đỉnh của nhánh `master` (hoặc `develop`, v.v.) hiện tại của bạn. Nếu điều đó hoạt động tốt, thì bạn có thể tua nhanh nhánh `master` của mình, và bạn sẽ có một lịch sử dự án tuyến tính. Cách khác để di chuyển công việc được giới thiệu từ nhánh này sang nhánh khác là cherry-pick nó. Một cherry-pick trong Git giống như một rebase cho một cam kết duy nhất. Nó lấy bản vá được giới thiệu trong một cam kết và cố gắng áp dụng lại nó trên nhánh bạn đang ở hiện tại. Điều này hữu ích nếu bạn có một số cam kết trong một nhánh chủ đề và bạn chỉ muốn tích hợp một trong số chúng, hoặc nếu bạn chỉ có một cam kết trong một nhánh chủ đề và bạn thích cherry-pick nó hơn là chạy rebase. Ví dụ, giả sử bạn có một dự án trông như thế này: .Lịch sử ví dụ trước khi cherry-pick image::images/rebase-cycle-1.png[Lịch sử ví dụ trước khi cherry-pick] Nếu bạn muốn kéo cam kết `e43a6` vào nhánh `master` của mình, bạn có thể chạy: [source,console]
$ git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf Finished one cherry-pick. [master]: created a0a41a9: "More friendly message" 3 files changed, 17 insertions(+), 3 deletions(-)
Điều này kéo cùng một thay đổi được giới thiệu trong `e43a6`, nhưng bạn nhận được một giá trị SHA-1 cam kết mới, bởi vì bạn đang áp dụng thay đổi tại một điểm khác trong lịch sử. Bây giờ lịch sử của bạn trông như thế này: .Lịch sử sau khi cherry-pick một cam kết từ một nhánh chủ đề image::images/rebase-cycle-2.png[Lịch sử sau khi cherry-pick một cam kết từ một nhánh chủ đề] Bây giờ bạn có thể xóa nhánh chủ đề của mình và bỏ các cam kết bạn không muốn kéo vào. ==== Rerere (((rerere))) Nếu bạn đang thực hiện nhiều việc hợp nhất và rebase, hoặc nếu bạn duy trì một nhánh chủ đề chạy dài, Git có một tính năng gọi là "rerere" có thể giúp ích. Rerere là viết tắt của "reuse recorded resolution" -- đó là một cách để giải quyết xung đột thủ công một cách ngắn gọn. Khi rerere được bật, Git sẽ giữ một tập hợp các hình ảnh trước và sau từ các lần hợp nhất thành công, và nếu nó thấy một xung đột trông giống hệt như một xung đột bạn đã sửa, nó sẽ chỉ sử dụng bản sửa lỗi từ lần trước, mà không làm phiền bạn với nó. Tính năng này có hai phần: một cài đặt cấu hình và một lệnh. Cài đặt cấu hình là `rerere.enabled`, bạn có thể đặt trong cấu hình toàn cục của mình để bắt đầu sử dụng nó: [source,console]
$ git config --global rerere.enabled true
Bây giờ, bất cứ khi nào bạn thực hiện một hợp nhất giải quyết các xung đột, giải pháp sẽ được ghi lại trong bộ đệm trong trường hợp bạn cần nó trong tương lai. Nếu bạn cần tương tác với bộ đệm rerere, bạn có thể sử dụng lệnh `git rerere`. Khi được gọi một mình, nó sẽ kiểm tra trạng thái giải quyết hợp nhất hiện tại và nếu cần giải quyết xung đột nhưng không có trong bộ đệm, nó sẽ không làm gì cả. Nếu một giải pháp có trong bộ đệm, nó sẽ giải quyết bất kỳ xung đột nào theo cách tương tự như đã làm trước đó. Cũng có một số lệnh con để xem những gì sẽ được ghi lại, để xóa trạng thái cụ thể khỏi bộ đệm, và để xóa toàn bộ bộ đệm. Chúng tôi sẽ đề cập đến rerere chi tiết hơn trong <<ch10-git-internals#ch10-rerere>>. ==== Gắn thẻ các Bản phát hành của bạn Khi bạn sẵn sàng thực hiện một bản phát hành, bạn có thể sẽ muốn tạo một thẻ để bạn có thể tạo lại bản phát hành đó bất cứ lúc nào trong tương lai. Bạn có thể tạo một thẻ mới như đã thảo luận trong <<ch02-git-basics#_git_tagging>>. Nếu bạn quyết định ký thẻ với tư cách là người bảo trì, việc gắn thẻ có thể trông giống như thế này: [source,console]
$ git tag -s v1.5 -m 'my signed 1.5 tag' You need a passphrase to unlock the secret key for user: "Scott Chacon <schacon@gmail.com>" 1024-bit DSA key, ID F721C45A, created 2009-02-09
Nếu bạn ký các thẻ của mình, bạn có vấn đề về việc phân phối khóa PGP công khai được sử dụng để ký chúng. Người bảo trì của dự án Git đã giải quyết vấn đề này bằng cách bao gồm khóa công khai của họ dưới dạng một blob trong kho lưu trữ và sau đó thêm một thẻ trỏ trực tiếp đến nội dung đó. Để làm điều này, bạn có thể tìm ra khóa nào bạn muốn bằng cách chạy `gpg --list-keys`: [source,console]
$ gpg --list-keys /Users/schacon/.gnupg/pubring.gpg
pub 1024D/F721C45A 2009-02-09 [expires: 2010-02-09]
uid Scott Chacon <schacon@gmail.com>
sub 2048g/45D02282 2009-02-09 [expires: 2010-02-09]
----
Sau đó, bạn có thể nhập trực tiếp khóa vào cơ sở dữ liệu Git bằng cách xuất nó và chuyển nó qua `git hash-object`, nó sẽ viết một blob mới với nội dung đó vào Git và trả về cho bạn SHA-1 của blob:
[source,console]
----
$ gpg -a --export F721C45A | git hash-object -w --stdin
659ef797d181633c87ec71ac3f9ba29fe5775b92
----
Bây giờ bạn đã có nội dung của khóa của mình trong Git, bạn có thể tạo một thẻ trỏ trực tiếp đến nó bằng cách chỉ định giá trị SHA-1 mới mà lệnh `hash-object` đã cho bạn:
[source,console]
----
$ git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92
----
Nếu bạn chạy `git push --tags`, thẻ `maintainer-pgp-pub` sẽ được chia sẻ với mọi người.
Nếu họ muốn xác minh một thẻ, họ có thể nhập khóa PGP của bạn bằng cách kéo blob trực tiếp ra khỏi cơ sở dữ liệu và nhập nó vào GPG:
[source,console]
----
$ git show maintainer-pgp-pub | gpg --import
----
Sau đó, họ có thể sử dụng khóa đó để xác minh tất cả các thẻ đã ký của bạn.
Ngoài ra, nếu bạn bao gồm các hướng dẫn trong thông điệp thẻ, việc chạy `git show <tag>` sẽ cho phép bạn cung cấp cho người dùng cuối các hướng dẫn cụ thể hơn về việc xác minh thẻ.
==== Tạo một Số bản dựng
Git không có một số tăng dần đơn điệu như "v123" hoặc tương đương với mỗi cam kết -- và vì lý do chính đáng.
Nếu bạn có một kho lưu trữ trung tâm duy nhất mà mọi người đều đẩy đến, sẽ dễ dàng có một số tăng dần cho mỗi cam kết.
Tuy nhiên, trong một hệ thống phân tán, bạn có thể có hai nhà phát triển thực hiện các cam kết cùng một lúc, và sẽ không có cách nào để biết số nào sẽ đến trước.
Trong Subversion, nếu bạn cam kết, bạn nhận được số 123; nếu tôi cam kết, tôi nhận được số 124.
Trong Git, cả hai chúng ta sẽ thực hiện một cam kết trên máy của riêng mình, mà không biết người kia đang làm gì.
Điều này có nghĩa là nếu bạn muốn có một số có thể đọc được bởi con người để sử dụng cho các cam kết của mình, bạn phải làm điều đó sau đó, khi bạn chia sẻ chúng với những người khác.
Bạn có thể làm điều đó ở phía máy chủ: có một tập lệnh chạy mỗi khi ai đó đẩy, nó sẽ tăng một số và liên kết nó với cam kết.
Hoặc, bạn có thể sử dụng một lệnh có thể giúp bạn với điều này.
Lệnh `git describe` sẽ lấy bất kỳ cam kết nào có thể truy cập được và cung cấp cho bạn một chuỗi có phần nào đó có thể đọc được bởi con người và sẽ không thay đổi.
Bằng cách này, bạn có thể nhận được một tên cho một ảnh chụp nhanh dễ hiểu hơn so với SHA-1 cam kết.
[source,console]
----
$ git describe master
v2.2.0-8-g5082d49
----
Git lấy tên của một ảnh chụp nhanh ở định dạng `<tag>-<num>-g<short_sha>`.
Đây là những gì nó trông như thế nào nếu bạn đã gắn thẻ cam kết bạn đang mô tả.
Nếu bạn chưa, nó sẽ tìm thẻ gần nhất và sử dụng thẻ đó thay thế.
Lệnh `git describe` phụ thuộc rất nhiều vào việc có các thẻ, vì vậy nếu bạn muốn sử dụng nó, bạn nên đảm bảo rằng bạn gắn thẻ các bản phát hành của mình một cách thích hợp.
Bạn cũng có thể sử dụng chuỗi này làm mục tiêu của lệnh `checkout` hoặc `show`, mặc dù nó dựa vào SHA-1 viết tắt ở cuối, vì vậy nó có thể không hợp lệ mãi mãi.
Ví dụ, nhân Linux gần đây đã tăng từ 8 lên 10 ký tự để đảm bảo tính duy nhất của các đối tượng SHA-1, vì vậy đầu ra `git describe` cũ hơn đã bị vô hiệu hóa.
==== Chuẩn bị một Bản phát hành
Bây giờ bạn muốn chuẩn bị một bản phát hành.
Bạn có một loạt công việc đã được đóng góp, và bạn muốn đóng gói tất cả lại để người khác sử dụng.
Vấn đề đầu tiên bạn sẽ gặp phải là Git không có hỗ trợ cho `svn export` hoặc `cvs export`.
Đây là một tính năng mà nhiều người muốn, nhưng Git muốn trở thành một hệ thống kiểm soát phiên bản, không phải là một công cụ tạo tarball.
Để có được một ảnh chụp nhanh của dự án của bạn mà không có thư mục `.git`, bạn có thể sử dụng `git archive`:
[source,console]
----
$ git archive master --prefix='project/' --format=zip > `git describe master`.zip
----
Nếu ai đó giải nén tệp đó, họ sẽ nhận được ảnh chụp nhanh mới nhất của dự án của bạn trong một thư mục `project`.
Bạn cũng có thể làm điều này với định dạng `tar`:
[source,console]
----
$ git archive master --prefix='project/' | gzip > `git describe master`.tar.gz
----
Bạn có thể làm điều này cho bất kỳ cam kết nào, không chỉ là cam kết mới nhất.
==== The Shortlog
Bây giờ là lúc để gửi email cho danh sách gửi thư của bạn để cho họ biết rằng bản phát hành mới nhất của bạn đã sẵn sàng.
Lệnh `git shortlog` có thể giúp bạn nhanh chóng tạo một bản ghi thay đổi về những gì đã được thêm vào dự án của bạn kể từ bản phát hành cuối cùng của bạn.
Nó tóm tắt tất cả các cam kết trong phạm vi bạn cung cấp; ví dụ, điều này sẽ cung cấp cho bạn một bản tóm tắt của tất cả các cam kết kể từ bản phát hành cuối cùng của bạn, nếu bản phát hành cuối cùng của bạn được đặt tên là v1.0.1:
[source,console]
----
$ git shortlog --no-merges master --not v1.0.1
Chris Lopez (1):
Fix a bug in the build system
Joel Wurtz (1):
Add 'gc --aggressive' to the manuals
Junio C Hamano (1):
Merge branch 'sd/gc-autodetect'
----
Bạn nhận được một bản tóm tắt sạch sẽ của tất cả các cam kết kể từ v1.0.1, được nhóm theo tác giả, mà bạn có thể gửi email cho danh sách của mình.
=== Tóm tắt
Bạn sẽ cảm thấy khá tự tin khi đóng góp vào một dự án sử dụng Git cũng như quản lý dự án của riêng bạn hoặc tích hợp các đóng góp từ người khác.
Chúc mừng — bạn đã trở thành một nhà phát triển Git hiệu quả!
Trong chương tiếp theo, bạn sẽ học cách sử dụng dịch vụ lưu trữ Git lớn nhất và phổ biến nhất: GitHub.
[[ch06-github]]
== GitHub
(((GitHub)))
GitHub là dịch vụ lưu trữ kho Git lớn nhất, là trung tâm cộng tác cho hàng triệu nhà phát triển và dự án.
Phần lớn các kho Git được lưu trữ trên GitHub, và nhiều dự án mã nguồn mở sử dụng nó cho việc lưu trữ Git, theo dõi issue, xem xét mã, và nhiều tính năng khác.
Mặc dù không phải là phần trực tiếp của dự án nguồn mở Git, rất có khả năng bạn sẽ cần tương tác với GitHub khi dùng Git trong môi trường chuyên nghiệp.
Chương này hướng dẫn cách sử dụng GitHub hiệu quả.
Chúng tôi sẽ trình bày cách đăng ký và quản lý tài khoản, tạo và sử dụng kho Git, các luồng công việc phổ biến để đóng góp vào dự án và nhận đóng góp, giao diện lập trình của GitHub và nhiều mẹo nhỏ hữu ích.
Nếu bạn không muốn dùng GitHub để lưu trữ dự án của mình hay cộng tác với các dự án trên GitHub, bạn có thể bỏ qua tới <<ch07-git-tools#ch07-git-tools>>.
[WARNING]
.Giao diện Thay đổi
====
Điều quan trọng là như nhiều trang web hoạt động khác, các thành phần giao diện trong ảnh chụp màn hình này có thể thay đổi theo thời gian.
Hy vọng ý tưởng tổng quát vẫn tương tự, nhưng nếu bạn muốn các ảnh chụp màn hình cập nhật hơn, phiên bản trực tuyến của cuốn sách có thể có các ảnh mới hơn.
====
[[_account_setup]]
=== Cài đặt và Cấu hình Tài khoản
(((GitHub, user accounts)))
Việc đầu tiên bạn cần làm là thiết lập một tài khoản người dùng miễn phí.
Chỉ cần truy cập https://github.com[^], chọn một tên người dùng chưa được sử dụng, cung cấp một địa chỉ email và một mật khẩu, và nhấp vào nút lớn màu xanh lá cây "`Sign up for GitHub`".
.Biểu mẫu đăng ký GitHub
image::images/signup.png[Biểu mẫu đăng ký GitHub]
Điều tiếp theo bạn sẽ thấy là trang giá cho các gói nâng cấp, nhưng bạn có thể bỏ qua điều này bây giờ.
GitHub sẽ gửi cho bạn một email để xác minh địa chỉ bạn đã cung cấp.
Hãy tiếp tục và làm điều này; nó khá quan trọng.
[[_ssh_access]]
==== Truy cập SSH
Tiếp theo, bạn nên thiết lập quyền truy cập SSH vào GitHub.
Nếu bạn chưa có khóa SSH, hãy xem <<_generate_ssh_key>>.
Mở cài đặt tài khoản của bạn bằng cách sử dụng liên kết ở góc trên cùng bên phải của cửa sổ:
.Liên kết "Cài đặt tài khoản"
image::images/account-settings.png[Liên kết "Cài đặt tài khoản"]
Sau đó chọn phần "SSH and GPG keys" ở bên trái.
.Liên kết "Khóa SSH"
image::images/ssh-keys.png[Liên kết "Khóa SSH"]
Từ đó, nhấp vào nút "`New SSH key`", đặt tên cho khóa của bạn, và dán nội dung của tệp `~/.ssh/id_rsa.pub` (hoặc bất cứ tên gì bạn đã đặt) vào vùng văn bản.
.Thêm một khóa SSH
image::images/add-key.png[Thêm một khóa SSH]
[NOTE]
====
Một thực hành tốt là đặt tên cho các khóa SSH của bạn.
Bạn có thể đặt cho mỗi khóa của mình một tên sẽ giúp bạn phân biệt chúng, như "`My Laptop`" hoặc "`Work`" nếu bạn muốn thu hồi chúng sau này.
====
==== Xác thực hai yếu tố
(((two-factor authentication)))
"Xác thực hai yếu tố", hay "2FA", là một cơ chế xác thực đang ngày càng trở nên phổ biến để giảm thiểu rủi ro tài khoản của bạn bị xâm phạm nếu mật khẩu của bạn bị đánh cắp.
Bật 2FA trên GitHub sẽ làm cho nó an toàn hơn, và nói chung là một ý tưởng tốt.
Để bật 2FA, hãy vào cài đặt tài khoản của bạn, và sau đó vào tab "Security".
.Tab "Bảo mật"
image::images/security-tab.png[Tab "Bảo mật"]
Ở đó, bạn có thể nhấp vào nút "Set up two-factor authentication", nó sẽ đưa bạn đến một trang cấu hình nơi bạn có thể chọn sử dụng một ứng dụng trên điện thoại của mình ("TOTP") hoặc để GitHub gửi cho bạn một mã qua SMS mỗi lần bạn cần đăng nhập.
.Thiết lập xác thực hai yếu tố
image::images/2fa-1.png[Thiết lập xác thực hai yếu tố]
Sau khi bạn chọn phương thức ưa thích của mình và làm theo các hướng dẫn để bật 2FA, tài khoản của bạn sẽ an toàn hơn một chút và bạn sẽ phải cung cấp một mã ngoài mật khẩu của mình mỗi lần bạn đăng nhập vào trang web GitHub.
==== Ảnh đại diện của bạn
Tiếp theo, nếu bạn muốn, bạn có thể thay thế ảnh đại diện được tạo bằng một hình ảnh bạn chọn.
Đầu tiên, hãy vào tab "`Profile`" trong cài đặt tài khoản của bạn.
.Tab "Hồ sơ"
image::images/profile-tab.png[Tab "Hồ sơ"]
Sau đó nhấp vào nút "`Upload new picture`" và cắt ảnh của bạn.
.Tải lên một ảnh đại diện
image::images/avatar-crop.png[Tải lên một ảnh đại diện]
==== Địa chỉ Email của bạn
GitHub sử dụng các địa chỉ email của bạn cho một vài việc.
Nếu bạn thiết lập nó, nó có thể gửi cho bạn các thông báo và các liên lạc khác qua email, và nó cũng sử dụng chúng để liên kết các cam kết Git của bạn với tài khoản của bạn.
Trong phần "Emails" của cài đặt tài khoản của bạn, bạn có thể đặt địa chỉ email chính mà GitHub sẽ gửi cho bạn thông tin liên lạc, và cũng có thể thêm các địa chỉ email khác sẽ được sử dụng để liên kết các cam kết của bạn với tài khoản của bạn.
.Cài đặt Emails
image::images/email-settings.png[Cài đặt Emails]
Bạn có thể đặt địa chỉ email chính của mình ở chế độ riêng tư, vì vậy nó sẽ không hiển thị cho bất kỳ ai.
Nếu bạn làm điều này, GitHub sẽ cung cấp cho bạn một địa chỉ email `username@users.noreply.github.com` mà bạn có thể sử dụng trong cấu hình Git của mình và nó sẽ chuyển hướng bất kỳ email nào được gửi đến nó đến địa chỉ riêng tư của bạn.
Xem <<_keeping_email_private>> để biết thêm chi tiết.
==== Các Dự án
Cuối cùng, để nhận điểm thưởng, bạn có thể kết nối tài khoản GitHub của mình với các dự án khác của bạn.
Nếu bạn có các dự án trên các dịch vụ khác, bạn có thể thêm chúng vào hồ sơ của mình.
.Phần "Dự án"
image::images/projects-section.png[Phần "Dự án"]
Nó sẽ hiển thị một danh sách các dự án phổ biến nhất của bạn trên GitHub và các dịch vụ khác cho bất kỳ ai xem hồ sơ của bạn.
[[_contributing_to_a_project]]
=== Đóng góp vào một Dự án
Bây giờ tài khoản của chúng ta đã được thiết lập, hãy cùng xem qua một số chi tiết có thể hữu ích trong việc giúp bạn đóng góp vào một dự án hiện có.
==== Phân nhánh các Dự án
(((forking)))
Nếu bạn muốn đóng góp vào một dự án hiện có mà bạn không có quyền đẩy, bạn có thể "`phân nhánh`" (fork) dự án.
Khi bạn "`phân nhánh`" một dự án, GitHub sẽ tạo một bản sao của dự án hoàn toàn là của bạn; nó nằm trong không gian tên của bạn, và bạn có thể đẩy vào đó.
Bằng cách này, các dự án không phải lo lắng về việc thêm người dùng làm cộng tác viên để cấp cho họ quyền đẩy.
Mọi người có thể phân nhánh một dự án, đẩy vào đó, và đóng góp các thay đổi của họ trở lại kho lưu trữ ban đầu bằng cách tạo ra một thứ gọi là "Yêu cầu Kéo" (Pull Request), mà chúng ta sẽ đề cập đến tiếp theo.
Điều này mở ra một chuỗi thảo luận với các thay đổi mã, và chủ sở hữu và người đóng góp sau đó có thể giao tiếp về thay đổi cho đến khi chủ sở hữu hài lòng với nó, tại thời điểm đó chủ sở hữu có thể hợp nhất nó vào.
Để phân nhánh một dự án, hãy truy cập trang của dự án và nhấp vào nút "`Fork`" ở góc trên cùng bên phải của trang.
.Nút "`Fork`"
image::images/forkbutton.png[Nút "`Fork`" ]
Sau vài giây, bạn sẽ được đưa đến trang dự án mới của mình, trang này sẽ có bản sao mã của bạn.
==== Luồng GitHub
(((GitHub, flow)))
GitHub được thiết kế xung quanh một quy trình làm việc cộng tác cụ thể, tập trung vào các Yêu cầu Kéo.
Luồng này hoạt động cho dù bạn đang cộng tác với một nhóm gắn bó chặt chẽ trong một kho lưu trữ được chia sẻ duy nhất, hay với một công ty phân tán toàn cầu hoặc mạng lưới những người lạ đóng góp vào một dự án mà bạn đang duy trì thông qua hàng tá các nhánh.
Nó tập trung vào quy trình làm việc <<ch03-git-branching#ch03-git-branching,Các nhánh Chủ đề>>.
Đây là bản chất chung về cách nó hoạt động:
1. Bạn tạo một nhánh từ `master`.
2. Bạn thực hiện một số cam kết.
3. Bạn mở một Yêu cầu Kéo trên GitHub.
4. Bạn thảo luận và xem xét mã với những người khác.
5. Bạn triển khai từ nhánh của mình để kiểm tra trong sản xuất.
6. Khi yêu cầu kéo được phê duyệt, bạn hợp nhất nó vào `master`.
Về cơ bản đây là <<_integration_manager_workflow,quy trình làm việc của Người quản lý Tích hợp>> mà chúng ta đã đề cập trong <<ch05-distributed-git#ch05-distributed-git>>, nhưng thay vì sử dụng email để giao tiếp và xem xét các thay đổi, nhóm sử dụng các công cụ dựa trên web của GitHub.
Hãy cùng xem một ví dụ về việc đề xuất một thay đổi cho một dự án mà chúng ta lưu trữ trên GitHub.
===== Tạo một Yêu cầu Kéo
Tony đang tìm kiếm mã để chạy trên bộ vi điều khiển có thể lập trình Arduino của mình và đã tìm thấy một chương trình tuyệt vời trên GitHub tại https://github.com/schacon/blink.
.Dự án sẽ được phân nhánh
image::images/blink-01-start.png[Dự án sẽ được phân nhánh]
Vấn đề duy nhất là tốc độ nhấp nháy quá nhanh; chúng tôi nghĩ rằng sẽ tốt hơn nhiều nếu đợi 3 giây thay vì 1 giây giữa mỗi lần thay đổi trạng thái.
Vì vậy, hãy cải thiện chương trình và gửi lại cho dự án như một thay đổi được đề xuất.
Đầu tiên, chúng ta sẽ nhấp vào nút "Fork" như chúng ta đã đề cập trước đó để có được bản sao của riêng mình của dự án.
Tên người dùng của chúng ta ở đây là "tonychacon", vì vậy bản sao của dự án này của chúng ta là tại `https://github.com/tonychacon/blink` và đó là nơi chúng ta có thể đẩy đến.
Chúng ta sẽ sao chép nó cục bộ, tạo một nhánh chủ đề, thay đổi mã và sau đó đẩy thay đổi đó trở lại GitHub.
[source,console]
----
$ git clone https://github.com/tonychacon/blink
Cloning into 'blink'...
$ cd blink
$ git checkout -b slowing-it-down
Switched to a new branch 'slowing-it-down'
$ sed -i 's/1000/3000/' blink.ino
$ git diff --word-diff
diff --git a/blink.ino b/blink.ino
index 1cb2a05..434b4b3 100644
--- a/blink.ino
+++ b/blink.ino
@@ -18,7 +18,7 @@ void loop() {
// turn the LED on (HIGH is the voltage level)
digitalWrite(led, HIGH);
// wait for a second
- [-delay(1000);-]{+delay(3000);+}
+ delay(3000);
// turn the LED off by making the voltage LOW
digitalWrite(led, LOW);
// wait for a second
- [-delay(1000);-]{+delay(3000);+}
+ delay(3000);
}
$ git commit -a -m 'slowing it down'
[slowing-it-down 5a4655d] slowing it down
1 file changed, 2 insertions(+), 2 deletions(-)
$ git push origin slowing-it-down
...
To https://github.com/tonychacon/blink
* [new branch] slowing-it-down -> slowing-it-down
----
Bây giờ chúng ta có thể truy cập nhánh của mình trên GitHub và chúng ta sẽ thấy rằng GitHub đã nhận thấy rằng chúng ta đã đẩy một nhánh chủ đề mới và hiển thị cho chúng ta một nút màu xanh lá cây lớn để kiểm tra các thay đổi của chúng ta và mở một Yêu cầu Kéo đến dự án ban đầu.
.Nút Yêu cầu Kéo mới
image::images/blink-02-pr.png[Nút Yêu cầu Kéo mới]
Bạn cũng có thể truy cập trang "Branches" tại `https://github.com/<user>/<project>/branches` để xác định vị trí nhánh của bạn và mở một Yêu cầu Kéo từ đó.
Nếu chúng ta nhấp vào nút màu xanh lá cây đó, chúng ta sẽ thấy một màn hình yêu cầu chúng ta đặt tiêu đề và mô tả cho Yêu cầu Kéo của mình.
Hầu như luôn luôn đáng để nỗ lực vào việc này, vì một mô tả tốt sẽ giúp chủ sở hữu của dự án ban đầu xác định những gì bạn đang cố gắng làm, liệu các thay đổi được đề xuất của bạn có chính xác hay không, và liệu việc chấp nhận chúng có cải thiện dự án ban đầu hay không.
Chúng ta cũng sẽ thấy một danh sách các cam kết trong nhánh chủ đề của chúng ta "ở phía trước" của nhánh `master` (trong trường hợp này, chỉ có một) và một diff thống nhất của tất cả các thay đổi sẽ được thực hiện nếu chủ sở hữu dự án hợp nhất nó.
.Trang tạo Yêu cầu Kéo
image::images/blink-03-pull-request-open.png[Trang tạo Yêu cầu Kéo]
Khi bạn nhấn nút "Create pull request", chủ sở hữu của dự án bạn đã phân nhánh sẽ nhận được một thông báo rằng ai đó đang đề xuất một thay đổi và một liên kết đến trang có tất cả thông tin về nó.
===== Lặp lại trên một Yêu cầu Kéo
Tại thời điểm này, chủ sở hữu dự án có thể xem xét thay đổi được đề xuất và hợp nhất nó, từ chối nó, hoặc bình luận về nó.
Giả sử họ thích ý tưởng này, nhưng muốn thời gian đèn tắt lâu hơn một chút so với thời gian đèn sáng.
Nơi cuộc trò chuyện này có thể diễn ra trong các quy trình làm việc chúng ta đã thấy trong <<ch05-distributed-git#ch05-distributed-git>> sẽ là qua email, trong quy trình làm việc mới của chúng ta, điều này diễn ra trực tuyến trên GitHub.
Chủ sở hữu dự án có thể xem xét diff thống nhất trên trang Yêu cầu Kéo và để lại một bình luận bằng cách nhấp vào một trong các dòng đã thay đổi.
.Bình luận về một dòng trong một Yêu cầu Kéo
image::images/blink-04-pr-comment.png[Bình luận về một dòng trong một Yêu cầu Kéo]
Khi người bảo trì đưa ra bình luận này, người mở Yêu cầu Kéo (và bất kỳ ai khác đang theo dõi kho lưu trữ) sẽ nhận được thông báo.
Chúng ta sẽ xem sau cách tùy chỉnh điều này, nhưng nếu họ đã bật thông báo qua mail, Tony sẽ nhận được một email về bình luận mới.
.Thông báo email về một bình luận Yêu cầu Kéo
image::images/blink-04-email.png[Thông báo email về một bình luận Yêu cầu Kéo]
Bất kỳ ai cũng có thể để lại các bình luận chung trên Yêu cầu Kéo.
Trên trang thảo luận Yêu cầu Kéo, chúng ta có thể thấy một ví dụ về chủ sở hữu dự án vừa bình luận về một dòng mã vừa để lại một bình luận chung trong phần thảo luận.
Bạn có thể thấy rằng bình luận mã cũng được đưa vào cuộc trò chuyện.
.Trang thảo luận Yêu cầu Kéo
image::images/blink-05-general-comment.png[Trang thảo luận Yêu cầu Kéo]
Bây giờ người đóng góp có thể thấy những gì họ cần làm để thay đổi của họ được chấp nhận.
May mắn thay, đây cũng là một việc rất đơn giản để làm.
Trong khi qua email, bạn có thể phải cuộn lại chuỗi của mình và gửi lại cho danh sách gửi thư, với GitHub, bạn chỉ cần cam kết lại vào nhánh chủ đề và đẩy.
[source,console]
----
$ sed -i 's/3000/2000/' blink.ino
$ git diff --word-diff
diff --git a/blink.ino b/blink.ino
index 434b4b3..4124ac7 100644
--- a/blink.ino
+++ b/blink.ino
@@ -21,7 +21,7 @@ void loop() {
digitalWrite(led, HIGH);
// wait for a second
delay(3000);
- [-delay(1000);-]{+delay(3000);+}
+ delay(2000);
// turn the LED off by making the voltage LOW
digitalWrite(led, LOW);
// wait for a second
- [-delay(1000);-]{+delay(3000);+}
+ delay(2000);
}
$ git commit -a -m 'have the LED be on longer'
[slowing-it-down 94392a0] have the LED be on longer
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
...
To https://github.com/tonychacon/blink
5a4655d..94392a0 slowing-it-down -> slowing-it-down
----
.Một lời "cảm ơn" trên trang Yêu cầu Kéo
image::images/blink-06-final.png[Một lời "cảm ơn" trên trang Yêu cầu Kéo]
Nếu bạn đẩy lại vào nhánh chủ đề của mình, Yêu cầu Kéo sẽ tự động được cập nhật để phản ánh các thay đổi mới bạn đã thực hiện.
Chủ sở hữu dự án sẽ thấy các thay đổi mới của bạn và có thể bình luận lại về chúng.
Điều thú vị cần lưu ý là nếu bạn xem tab "Files Changed" trên Yêu cầu Kéo này, bạn sẽ nhận được diff "thống nhất" -- nghĩa là, tổng chênh lệch tổng hợp sẽ được giới thiệu vào nhánh `master` của bạn nếu nhánh chủ đề này được hợp nhất.
Theo thuật ngữ `git diff`, điều này về cơ bản cho bạn thấy điều tương tự như `git diff master...<branch-name>`.
Xem <<_determining_what_is_introduced>> để biết thêm về loại diff này.
Điều khác bạn sẽ nhận thấy là GitHub kiểm tra xem Yêu cầu Kéo có hợp nhất sạch sẽ hay không và cung cấp một nút để thực hiện việc hợp nhất cho bạn trên máy chủ.
Nút này chỉ hiển thị nếu bạn có quyền ghi vào kho lưu trữ và có thể thực hiện một hợp nhất tầm thường.
Nếu bạn nhấp vào nó, GitHub sẽ thực hiện một hợp nhất "không tua đi nhanh", nghĩa là ngay cả khi việc hợp nhất _có thể_ là một tua đi nhanh, nó vẫn sẽ tạo ra một cam kết hợp nhất.
Nếu bạn muốn chỉ kéo công việc vào và hợp nhất nó cục bộ, bạn có thể làm điều đó.
Nếu bạn hợp nhất nhánh này vào nhánh `master` và đẩy nó lên GitHub, Yêu cầu Kéo sẽ tự động bị đóng.
Đây là quy trình làm việc cơ bản mà hầu hết các dự án GitHub sử dụng.
Các nhánh chủ đề được tạo, các Yêu cầu Kéo được mở trên chúng, một cuộc thảo luận diễn ra, nhiều công việc hơn có thể được thêm vào nhánh và Yêu cầu Kéo cuối cùng bị đóng (hy vọng là được hợp nhất).
[NOTE]
====
Điều quan trọng cần lưu ý là bạn có thể mở một Yêu cầu Kéo giữa bất kỳ hai nhánh nào trong kho lưu trữ.
Nếu bạn muốn cộng tác với ai đó về một tính năng, bạn có thể mở một Yêu cầu Kéo với nhánh của họ trong tên nhánh của bạn.
Bằng cách này, bạn có thể thảo luận về các thay đổi trong Yêu cầu Kéo mà không cần phải hợp nhất chúng vào `master` trước.
====
==== Các Yêu cầu Kéo Nâng cao
Bây giờ chúng ta đã đề cập đến những điều cơ bản về việc đóng góp vào một dự án trên GitHub, hãy cùng đề cập đến một vài mẹo và thủ thuật thú vị hơn về các Yêu cầu Kéo để bạn có thể sử dụng chúng hiệu quả hơn.
===== Các Yêu cầu Kéo như các Bản vá
Điều quan trọng cần hiểu là nhiều dự án không thực sự nghĩ về các Yêu cầu Kéo như các hàng đợi các bản vá hoàn hảo sẽ được áp dụng sạch sẽ theo thứ tự, như hầu hết các dự án dựa trên danh sách gửi thư làm.
Hầu hết các dự án GitHub nghĩ về các nhánh Yêu cầu Kéo như một cuộc trò chuyện lặp đi lặp lại xung quanh một thay đổi được đề xuất, mà đỉnh điểm là một diff thống nhất được áp dụng bằng cách hợp nhất.
Đây là một sự khác biệt quan trọng, bởi vì nói chung, thay đổi được đề xuất trước khi mã được coi là hoàn hảo, điều này hiếm hơn nhiều với các đóng góp chuỗi bản vá dựa trên danh sách gửi thư.
Điều này cho phép một cuộc trò chuyện sớm hơn với những người bảo trì, để công việc đi đến một giải pháp phù hợp là một nỗ lực cộng đồng nhiều hơn.
Khi một thay đổi được đề xuất thông qua một Yêu cầu Kéo và những người bảo trì hoặc cộng đồng đề xuất một thay đổi, chuỗi bản vá thường không được cuộn lại, mà thay vào đó, sự khác biệt được đẩy như một cam kết mới vào nhánh, đưa cuộc trò chuyện tiến lên với bối cảnh mới của công việc trước đó còn nguyên vẹn.
Ví dụ, nếu bạn quay lại và xem lại <<blink-06-final>>, bạn sẽ nhận thấy rằng người đóng góp đã không rebase cam kết của họ và đẩy lại.
Thay vào đó, họ đã thêm một cam kết mới và đẩy nó vào nhánh hiện có.
Bằng cách này, nếu bạn quay lại và xem Yêu cầu Kéo này trong tương lai, bạn có thể dễ dàng tìm thấy tất cả bối cảnh tại sao các quyết định được đưa ra.
Việc nhấp vào nút "Merge" trên trang web một cách có chủ đích sẽ tạo ra một cam kết hợp nhất tham chiếu đến Yêu cầu Kéo để dễ dàng quay lại và nghiên cứu Yêu cầu Kéo ban đầu nếu cần.
===== Theo kịp Upstream
Nếu một Yêu cầu Kéo trở nên lỗi thời hoặc không hợp nhất sạch sẽ, bạn sẽ muốn sửa nó để người bảo trì có thể dễ dàng hợp nhất nó.
GitHub sẽ kiểm tra điều này cho bạn và cho bạn biết ở cuối mỗi Yêu cầu Kéo nếu việc hợp nhất là tầm thường hay không.
.Yêu cầu Kéo không hợp nhất sạch sẽ
image::images/pr-does-not-merge-cleanly.png[Yêu cầu Kéo không hợp nhất sạch sẽ]
Nếu bạn thấy một cái gì đó như thế này, bạn sẽ muốn sửa nhánh của mình để nó hợp nhất sạch sẽ.
Có hai cách chính để làm điều này.
Bạn có thể rebase nhánh của mình trên đỉnh của bất kỳ nhánh mục tiêu nào (thường là nhánh `master` của kho lưu trữ ngược dòng), hoặc bạn có thể hợp nhất nhánh mục tiêu vào nhánh của mình.
Hầu hết các nhà phát triển trên GitHub sẽ chọn làm cái sau, vì những lý do tương tự chúng ta vừa xem xét trong phần trước.
Điều quan trọng là trạng thái hợp nhất cuối cùng và lịch sử để đến đó; việc rebase chỉ đơn giản là cung cấp cho bạn một lịch sử sạch hơn một chút, nhưng cũng khó khăn hơn nhiều và dễ xảy ra lỗi.
Nếu bạn muốn hợp nhất trong nhánh mục tiêu để làm cho Yêu cầu Kéo của bạn có thể hợp nhất được, bạn sẽ thêm kho lưu trữ ban đầu làm một điều khiển từ xa mới, tìm nạp từ nó, hợp nhất nhánh chính từ kho lưu trữ đó vào nhánh chủ đề của bạn, khắc phục mọi sự cố và sau đó cuối cùng đẩy nó trở lại cùng một nhánh bạn đã mở Yêu cầu Kéo.
Ví dụ, giả sử rằng trong ví dụ "tonychacon" từ trước, tác giả ban đầu đã thực hiện một thay đổi sẽ tạo ra một xung đột trong Yêu cầu Kéo.
Hãy cùng xem qua quá trình đó.
[source,console]
----
$ git clone https://github.com/tonychacon/blink
Cloning into 'blink'...
$ cd blink
$ git remote add upstream https://github.com/schacon/blink
$ git fetch upstream
...
From https://github.com/schacon/blink
* [new branch] master -> upstream/master
----
Bây giờ chúng ta sẽ hợp nhất nhánh `master` ngược dòng vào nhánh `slowing-it-down` của chúng ta.
[source,console]
----
$ git checkout slowing-it-down
Switched to branch 'slowing-it-down'
$ git merge upstream/master
...
Auto-merging blink.ino
CONFLICT (content): Merge conflict in blink.ino
Automatic merge failed; fix conflicts and then commit the result.
----
Bây giờ chúng ta có một xung đột hợp nhất.
.Nhánh có xung đột hợp nhất
image::images/pr-merge-conflict.png[Nhánh có xung đột hợp nhất]
Chúng ta cần giải quyết xung đột.
Khi chúng ta đã làm xong, chúng ta sẽ cần sử dụng `git add` trên tệp bị xung đột để tổ chức nó, và sau đó `git commit` để tạo một cam kết hợp nhất mới.
[source,console]
----
$ vim blink.ino
$ git diff --word-diff
diff --git a/blink.ino b/blink.ino
index 4124ac7..252d43a 100644
--- a/blink.ino
+++ b/blink.ino
@@ -1,5 +1,6 @@
// Pin 13 has an LED connected on most Arduino boards.
// give it a name:
int led = 13;
+String message = "Hello, world!";
// the setup routine runs once when you press reset:
void setup() {
@@ -21,7 +22,7 @@ void loop() {
digitalWrite(led, HIGH);
// wait for a second
delay(3000);
- [-delay(1000);-]{+delay(3000);+}
+ delay(2000);
// turn the LED off by making the voltage LOW
digitalWrite(led, LOW);
// wait for a second
- [-delay(1000);-]{+delay(3000);+}
+ delay(2000);
}
$ git status
On branch slowing-it-down
Your branch is ahead of 'origin/slowing-it-down' by 1 commit.
(use "git push" to publish your local commits)
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: blink.ino
$ git commit
[slowing-it-down 2717293] Merge remote-tracking branch 'upstream/master' \
into slowing-it-down
$ git push
...
To https://github.com/tonychacon/blink
94392a0..2717293 slowing-it-down -> slowing-it-down
----
Sau đó, chúng ta chỉ cần đẩy công việc mới lên cùng một nhánh trên GitHub và Yêu cầu Kéo sẽ được cập nhật lại.
.Yêu cầu Kéo có thể hợp nhất lại
image::images/pr-is-mergeable-again.png[Yêu cầu Kéo có thể hợp nhất lại]
Một trong những điều tuyệt vời về Git là bạn có thể làm điều này liên tục.
Nếu bạn có một dự án chạy rất dài, bạn có thể dễ dàng hợp nhất từ nhánh mục tiêu nhiều lần và chỉ phải giải quyết các xung đột đã phát sinh kể từ lần hợp nhất cuối cùng, làm cho quá trình này rất dễ quản lý.
Nếu bạn hoàn toàn muốn rebase nhánh để dọn dẹp lịch sử của mình, bạn chắc chắn có thể làm như vậy, nhưng rất khuyến khích không nên đẩy ép lên nhánh mà Yêu cầu Kéo đã được mở.
Nếu những người khác đã kéo nó xuống và làm thêm công việc trên đó, bạn sẽ gặp phải tất cả các vấn đề được trình bày trong <<ch03-git-branching#_rebase_peril>>.
Thay vào đó, hãy đẩy nhánh đã rebase vào một nhánh mới trên GitHub và mở một Yêu cầu Kéo hoàn toàn mới tham chiếu đến cái cũ, sau đó đóng cái ban đầu.
===== Các Tham chiếu
Câu hỏi tiếp theo của bạn có thể là "Làm cách nào để tham chiếu đến Yêu cầu Kéo?".
Nếu bạn đang tham chiếu đến một Yêu cầu Kéo trong một Yêu cầu Kéo khác, hoặc trong một Vấn đề, bạn có thể chỉ cần đặt số của nó vào một bình luận với một dấu `#` ở phía trước.
Vì vậy, `#12` sẽ tham chiếu đến Yêu cầu Kéo hoặc Vấn đề #12.
Bạn cũng có thể cụ thể hơn.
Nếu bạn viết `user#12`, nó sẽ tham chiếu đến một Yêu cầu Kéo hoặc Vấn đề trong nhánh của người dùng đó của kho lưu trữ hiện tại.
Nếu bạn viết `user/repo#12`, nó sẽ tham chiếu đến một Yêu cầu Kéo hoặc Vấn đề trong kho lưu trữ `user/repo`.
Bạn cũng có thể tham chiếu đến một cam kết cụ thể.
Một băm SHA-1 đầy đủ 40 ký tự sẽ được liên kết, nhưng GitHub cũng sẽ tự động liên kết các băm ngắn hơn.
Nếu bạn muốn tham chiếu đến một cam kết trong một nhánh khác, bạn có thể sử dụng cú pháp `user@commit` tương tự như với các Yêu cầu Kéo, hoặc `user/repo@commit` để tham chiếu đến một cam kết trong một kho lưu trữ khác.
Hãy xem một ví dụ về điều này.
Trong <<pr-references>> chúng ta đã lấy ví dụ từ trước và tham chiếu chéo đến một Yêu cầu Kéo trước đó, một cam kết trong một nhánh và một cam kết trong cùng một dự án.
.Các tham chiếu chéo trong một bình luận Yêu cầu Kéo
image::images/pr-references.png[Các tham chiếu chéo trong một bình luận Yêu cầu Kéo]
Khi chúng ta gửi bình luận đó, nó sẽ trông giống như <<pr-references-rendered>>.
.Các tham chiếu chéo được hiển thị trong một bình luận Yêu cầu Kéo
image::images/pr-references-rendered.png[Các tham chiếu chéo được hiển thị trong một bình luận Yêu cầu Kéo]
Lưu ý rằng băm SHA-1 đầy đủ chúng ta đã đặt vào đó đã được rút ngắn chỉ còn 7 ký tự đầu tiên.
Nếu bạn liên kết đến một Vấn đề hoặc Yêu cầu Kéo khác, GitHub sẽ hiển thị một tham chiếu trong dòng thời gian của Vấn đề hoặc Yêu cầu Kéo đó.
Điều này giúp dễ dàng theo dõi cuộc trò chuyện và tiến độ.
.Các tham chiếu chéo được hiển thị trong dòng thời gian Yêu cầu Kéo
image::images/pr-references-timeline.png[Các tham chiếu chéo được hiển thị trong dòng thời gian Yêu cầu Kéo]
==== GitHub Flavored Markdown
(((GitHub, Flavored Markdown)))
Cũng rất tuyệt khi có thể sử dụng Markdown ở những nơi bạn muốn có thể định dạng văn bản của mình hoặc thêm hình ảnh.
GitHub lấy Markdown thông thường và thêm một vài thứ nữa vào nó, gọi nó là "GitHub Flavored Markdown".
===== Danh sách Công việc
Một trong những tính năng bổ sung hữu ích hơn là Danh sách Công việc.
Một danh sách công việc là một danh sách các hộp kiểm của những việc bạn muốn hoàn thành.
Đặt chúng trong một Vấn đề hoặc Yêu cầu Kéo thường sẽ hiển thị chúng với các hộp kiểm nơi bạn có thể đánh dấu chúng.
[source]
----
- [x] Viết mã
- [ ] Viết tất cả các bài kiểm tra
- [ ] Tài liệu hóa mã
----
Nếu chúng ta đặt điều này trong mô tả của một Yêu cầu Kéo hoặc Vấn đề, nó sẽ được hiển thị như <<task-list-rendered>>.
[[task-list-rendered]]
.Danh sách công việc được hiển thị
image::images/task-list-rendered.png[Danh sách công việc được hiển thị]
Điều này thường được sử dụng trong các Yêu cầu Kéo để chỉ ra tất cả những gì cần phải được thực hiện trên nhánh trước khi nó sẵn sàng để hợp nhất.
Phần thực sự thú vị là bạn thực sự có thể chỉ cần nhấp vào các hộp kiểm để cập nhật bình luận -- bạn không cần phải chỉnh sửa trực tiếp Markdown để đánh dấu các tác vụ.
Hơn nữa, GitHub sẽ tìm kiếm các danh sách công việc trong các Vấn đề và Yêu cầu Kéo của bạn và hiển thị chúng dưới dạng siêu dữ liệu trên các trang liệt kê chúng.
Ví dụ, nếu bạn có một Yêu cầu Kéo có các tác vụ và bạn xem trang tổng quan của tất cả các Yêu cầu Kéo, bạn có thể thấy phần trăm các tác vụ đã hoàn thành.
.Tóm tắt danh sách công việc trên trang danh sách Yêu cầu Kéo
image::images/task-list-in-pr-list.png[Tóm tắt danh sách công việc trên trang danh sách Yêu cầu Kéo]
Điều này giúp mọi người chia nhỏ công việc của một Yêu cầu Kéo thành các nhiệm vụ phụ nhỏ hơn và giúp những người khác theo dõi tiến độ của nhánh.
===== Đoạn mã
Bạn cũng có thể thêm các đoạn mã vào các bình luận.
Điều này đặc biệt hữu ích nếu bạn muốn trình bày một cái gì đó mà bạn _có thể_ thử làm trước khi bạn thực sự thực hiện nó như một cam kết.
Nó cũng thường được sử dụng để thêm mã ví dụ về những gì không hoạt động hoặc Yêu cầu Kéo này sẽ hoàn thành những gì.
Để thêm một đoạn mã, bạn cần "rào" nó bằng các dấu backtick.
[source]
----
```java
System.out.println("Hello, world!");
```
----
Nếu bạn thêm một tên ngôn ngữ như chúng ta đã làm ở đó với "java", GitHub cũng sẽ cố gắng làm nổi bật cú pháp của đoạn mã.
Trong trường hợp của ví dụ trên, nó sẽ hiển thị như <<code-snippet-rendered>>.
[[code-snippet-rendered]]
.Ví dụ về mã được rào được hiển thị
image::images/code-snippet-rendered.png[Ví dụ về mã được rào được hiển thị]
===== Trích dẫn
Nếu bạn đang trả lời một phần nhỏ của một bình luận dài, bạn có thể trích dẫn có chọn lọc từ nó bằng cách đặt trước các dòng bằng ký tự `>`.
Trên thực tế, điều này rất phổ biến và rất hữu ích đến nỗi có một phím tắt cho nó.
Nếu bạn tô sáng văn bản trong một bình luận mà bạn muốn trả lời và nhấn phím `r`, nó sẽ trích dẫn văn bản đó trong hộp bình luận của bạn cho bạn.
Các trích dẫn sẽ trông giống như thế này:
[source]
----
> Nó rất phổ biến và rất hữu ích đến nỗi có một phím tắt cho nó.
Vâng, tôi biết, phải không?
----
Sau khi được hiển thị, bình luận sẽ trông giống như <<quoting-rendered>>.
[[quoting-rendered]]
.Trích dẫn được hiển thị
image::images/quoting-rendered.png[Trích dẫn được hiển thị]
===== Biểu tượng cảm xúc
(((emoji)))
Cuối cùng, bạn có thể sử dụng biểu tượng cảm xúc trong các bình luận của mình.
Điều này thực sự được sử dụng khá rộng rãi trong các bình luận bạn sẽ thấy trên nhiều Vấn đề và Yêu cầu Kéo của GitHub.
Thậm chí còn có một trình trợ giúp biểu tượng cảm xúc trong GitHub.
Nếu bạn đang nhập một bình luận và bạn nhập một dấu `:`, một trình tự động hoàn thành sẽ giúp bạn tìm thấy những gì bạn đang tìm kiếm.
.Trình tự động hoàn thành biểu tượng cảm xúc
image::images/emoji-completion.png[Trình tự động hoàn thành biểu tượng cảm xúc]
Các biểu tượng cảm xúc có dạng `:<name>:` trong bình luận.
Ví dụ, chúng ta có thể viết một cái gì đó như thế này:
[source]
----
Tôi đang :smile:. Nhưng tôi cũng đang :rage: và :cry:. :trollface:
----
Sau khi được hiển thị, nó sẽ trông giống như <<emoji-rendered>>.
[[emoji-rendered]]
.Biểu tượng cảm xúc được hiển thị
image::images/emoji-rendered.png[Biểu tượng cảm xúc được hiển thị]
Mặc dù không hoàn toàn cần thiết, điều này thêm một yếu tố vui vẻ và cảm xúc vào một phương tiện mà nếu không sẽ khó truyền đạt cảm xúc.
[NOTE]
====
Đây hiện không phải là một phần của đặc tả GitHub Flavored Markdown tiêu chuẩn, mà là một tính năng của trang web GitHub.com.
Có thể nó sẽ trở thành một phần của đặc tả trong tương lai.
====
==== Hình ảnh
Cũng có thể nhúng hình ảnh vào các bình luận.
Nếu bạn kéo và thả một tệp hình ảnh vào hộp bình luận, nó sẽ được tải lên một CDN và một liên kết Markdown sẽ được chèn để hiển thị nó.
.Hình ảnh được hiển thị
image::images/image-rendered.png[Hình ảnh được hiển thị]
Bạn có thể xem một ví dụ trong <<image-rendered>>.
Điều này có thể rất hữu ích để giải thích các vấn đề phức tạp hoặc chứng minh một thay đổi được đề xuất sẽ trông như thế nào.
==== Giữ Địa chỉ Email của bạn ở chế độ Riêng tư
(((GitHub, keeping email address private)))
GitHub cũng cho phép bạn giữ địa chỉ email thực của mình ở chế độ riêng tư.
Nếu bạn chọn làm như vậy, GitHub sẽ cung cấp cho bạn một địa chỉ email "noreply" (`<username>@users.noreply.github.com`) mà bạn có thể đặt làm địa chỉ email cam kết của mình, và bất kỳ email nào được gửi đến địa chỉ đó sẽ được chuyển hướng đến địa chỉ email chính (riêng tư) của bạn.
Trong phần "Emails" của cài đặt tài khoản của bạn, bạn có thể chọn hộp "Keep my email address private" để bật tính năng này.
.Giữ địa chỉ email của tôi ở chế độ riêng tư
image::images/keep-email-private.png[Giữ địa chỉ email của tôi ở chế de riêng tư]
Nếu bạn làm điều này, có một vài nơi bạn có thể muốn thay đổi cấu hình Git của mình để sử dụng địa chỉ này thay vì địa chỉ thực của bạn.
Cụ thể, bạn sẽ muốn thay đổi `user.email` trong cấu hình Git của mình, và bạn có thể cũng sẽ muốn đặt lại các địa chỉ email trong tệp `.mailmap` của mình nếu bạn có.
Ví dụ:
[source,ini]
----
[user]
name = Tony Chacon
email = tonychacon@users.noreply.github.com
----
Bây giờ các cam kết của bạn sẽ được tác giả bởi `tonychacon@users.noreply.github.com` thay vì địa chỉ email thực của bạn.
[NOTE]
====
Địa chỉ email bạn sử dụng cho các cam kết của mình không giống với địa chỉ bạn sử dụng để đăng nhập vào GitHub.
Bạn vẫn có thể đăng nhập bằng địa chỉ email chính của mình ngay cả khi bạn chọn giữ nó ở chế độ riêng tư.
====
[[_maintaining_gh_project]]
=== Duy trì một Dự án
Bây giờ chúng ta đã quen với việc đóng góp vào một dự án, hãy cùng xem xét mặt còn lại: tạo, duy trì và quản trị dự án của riêng bạn.
==== Tạo một Kho lưu trữ Mới
Hãy tạo một kho lưu trữ mới để chia sẻ mã dự án của chúng ta.
Bắt đầu bằng cách nhấp vào nút "`New repository`" ở phía bên phải của bảng điều khiển, hoặc từ nút `+` trên thanh công cụ trên cùng bên cạnh tên người dùng của bạn như đã thấy trong <<_new_repo_dropdown>>.
[[_new_repo_dropdown]]
.Trình đơn thả xuống "New repository"
image::images/new-repo-dropdown.png[Trình đơn thả xuống "New repository" ]
Điều này sẽ đưa bạn đến biểu mẫu "kho lưu trữ mới":
.Biểu mẫu "kho lưu trữ mới"
image::images/new-repo.png[Biểu mẫu "kho lưu trữ mới" ]
Tất cả những gì biểu mẫu này thực sự yêu cầu là một tên dự án; các trường khác đều là tùy chọn.
Hiện tại, chỉ cần nhấp vào nút "`Create Repository`", và bùm -- bạn có một kho lưu trữ mới trên GitHub, có tên là `project`.
Vì bạn chưa có mã nào ở đó, GitHub sẽ hiển thị cho bạn các hướng dẫn về cách tạo một kho lưu trữ mới trên máy cục bộ của bạn và đẩy nó lên, cách đẩy lên một kho lưu trữ Git hiện có, hoặc cách nhập một kho lưu trữ từ một kho lưu trữ Subversion như đã thấy trong <<_repo_quick_setup>>.
[[_repo_quick_setup]]
.Hướng dẫn thiết lập nhanh cho một kho lưu trữ mới
image::images/repo-quick-setup.png[Hướng dẫn thiết lập nhanh cho một kho lưu trữ mới]
Những hướng dẫn này tương tự như những gì chúng ta đã xem qua.
Để khởi tạo một dự án chưa được phiên bản, bạn sẽ sử dụng:
[source,console]
----
$ git init
$ git add .
$ git commit -m 'initial commit'
$ git remote add origin https://github.com/testinguser/project.git
$ git push -u origin master
----
Bây giờ bạn có một kho lưu trữ Git trên GitHub, và một bản sao cục bộ của nó đã được kiểm xuất.
Bạn có thể đẩy lên nó bất cứ khi nào bạn muốn.
==== Thêm Cộng tác viên
Bây giờ giả sử bạn muốn làm việc với một số người.
Nếu bạn muốn cấp cho người khác quyền đẩy vào kho lưu trữ của mình, bạn phải thêm họ làm "cộng tác viên".
Nếu Ben, Tony, và Jessica đăng ký tài khoản trên GitHub, và bạn muốn cấp cho họ quyền đẩy vào kho lưu trữ của mình, bạn có thể thêm họ vào dự án của mình.
Làm như vậy sẽ cấp cho họ quyền đẩy, có nghĩa là họ có cả quyền đọc và ghi vào dự án và kho lưu trữ Git.
Nhấp vào liên kết "`Settings`" ở cuối thanh bên phải.
.Liên kết "Cài đặt" của kho lưu trữ
image::images/repo-settings-link.png[Liên kết "Cài đặt" của kho lưu trữ]
Sau đó chọn "`Collaborators`" từ menu bên trái.
Sau đó, nhập tên người dùng vào hộp và nhấp vào "`Add collaborator`."
.Thêm một cộng tác viên
image::images/repo-collaborators.png[Thêm một cộng tác viên]
Bạn có thể lặp lại điều này bao nhiêu lần tùy thích để cấp quyền truy cập cho mọi người bạn muốn; bạn có thể thấy tất cả những người có quyền truy cập trong danh sách cộng tác viên.
.Một danh sách các cộng tác viên
image::images/repo-collaborator-list.png[Một danh sách các cộng tác viên]
Nếu bạn cần thu hồi quyền truy cập của ai đó, chỉ cần nhấp vào "X" ở bên phải hàng của họ.
==== Quản lý các Yêu cầu Kéo
Bây giờ bạn có một dự án với một số mã trong đó và có thể cả một vài cộng tác viên có quyền đẩy, hãy cùng xem phải làm gì khi bạn tự nhận được một Yêu cầu Kéo.
Các Yêu cầu Kéo có thể đến từ một nhánh trong một nhánh của kho lưu trữ của bạn, hoặc chúng có thể đến từ một nhánh khác trong cùng một kho lưu trữ.
Sự khác biệt duy nhất là những yêu cầu từ một nhánh thường đến từ những người mà bạn không thể đẩy vào nhánh của họ và họ không thể đẩy vào nhánh của bạn, trong khi với các Yêu cầu Kéo nội bộ, cả hai bên thường có thể truy cập vào nhánh.
Đối với những ví dụ này, giả sử bạn là "tonychacon" và bạn đã tạo một dự án Arduino mới có tên là "blink".
===== Thông báo Yêu cầu Kéo
Ai đó đến, phân nhánh dự án của bạn, thực hiện một thay đổi và mở một Yêu cầu Kéo.
Bạn sẽ nhận được một email về Yêu cầu Kéo mới sẽ trông giống như <<_pr_notification_email>>.
[[_pr_notification_email]]
.Email thông báo Yêu cầu Kéo
image::images/pr-notification-email.png[Email thông báo Yêu cầu Kéo]
Có một vài điều cần chú ý về email này.
Nó sẽ cung cấp cho bạn một diffstat nhỏ -- một danh sách các tệp đã thay đổi trong Yêu cầu Kéo và mức độ thay đổi.
Nó cung cấp cho bạn một liên kết đến Yêu cầu Kéo trên GitHub.
Nó cũng cung cấp cho bạn một vài URL mà bạn có thể sử dụng từ dòng lệnh.
Nếu bạn để ý dòng chữ `git pull <url> patch-1`, đây là một cách đơn giản để hợp nhất một nhánh từ xa mà không cần phải thêm một điều khiển từ xa.
Chúng ta đã xem qua điều này một cách ngắn gọn trong <<_checking_out_remote_branches>>.
Nếu bạn muốn, bạn có thể tạo và chuyển sang một nhánh chủ đề và sau đó chạy lệnh này để hợp nhất các thay đổi của Yêu cầu Kéo.
Các URL thú vị khác là các URL `.diff` và `.patch`, như bạn có thể đoán, cung cấp các phiên bản diff thống nhất và bản vá của Yêu cầu Kéo.
Về mặt kỹ thuật, bạn có thể hợp nhất công việc của Yêu cầu Kéo với một cái gì đó như thế này:
[source,console]
----
$ curl https://github.com/tonychacon/blink/pull/1.patch | git am
----
===== Thử nghiệm trong một Nhánh Yêu cầu Kéo
Như chúng ta đã đề cập trong <<_github_flow>>, bây giờ bạn có thể có một cuộc trò chuyện với người đã mở Yêu cầu Kéo.
Bạn có thể bình luận về các dòng mã cụ thể, bình luận về toàn bộ các cam kết hoặc bình luận về chính Yêu cầu Kéo, sử dụng GitHub Flavored Markdown ở mọi nơi.
Mỗi khi ai đó khác bình luận về Yêu cầu Kéo, bạn sẽ tiếp tục nhận được thông báo qua email để bạn biết rằng có hoạt động đang diễn ra.
Mỗi email sẽ có một liên kết đến Yêu cầu Kéo nơi hoạt động đang diễn ra, và bạn cũng có thể trả lời trực tiếp email để bình luận về chuỗi Yêu cầu Kéo.
.Trả lời một thông báo email
image::images/pr-email-resp.png[Trả lời một thông báo email]
Khi mã ở một nơi bạn thích và muốn hợp nhất nó vào, bạn có thể kéo mã xuống và hợp nhất nó cục bộ, hoặc bạn có thể sử dụng cú pháp `git pull <url> <branch>` mà chúng ta đã thấy trước đó, hoặc bạn có thể thêm nhánh làm một điều khiển từ xa, tìm nạp và hợp nhất.
Nếu việc hợp nhất là tầm thường, bạn cũng có thể chỉ cần nhấn nút "Merge" trên trang GitHub.
Điều này sẽ thực hiện một hợp nhất "không tua đi nhanh", tạo ra một cam kết hợp nhất ngay cả khi có thể thực hiện một hợp nhất tua đi nhanh.
Điều này có nghĩa là mỗi khi bạn nhấn nút hợp nhất, một cam kết hợp nhất được tạo ra, bất kể có hay không một cam kết sẽ được tạo ra nếu bạn làm điều đó trên dòng lệnh.
Xem <<_merge_button>>.
[[_merge_button]]
.Nút "Merge"
image::images/pr-merge-button.png[Nút "Merge" ]
Nếu bạn quyết định không muốn hợp nhất nó, bạn có thể chỉ cần đóng Yêu cầu Kéo và người đã mở nó sẽ được thông báo.
===== Các Tham chiếu Yêu cầu Kéo
Nếu bạn nhận được _rất nhiều_ Yêu cầu Kéo và không muốn thêm một loạt các điều khiển từ xa hoặc thực hiện các lần kéo một lần mỗi lần, có một mẹo thông minh bạn có thể sử dụng để tìm nạp từ tất cả các nhánh.
Đây là một mẹo Git hơi nâng cao, nhưng nó có thể khá hữu ích.
Git có thể tìm nạp các nhánh từ xa vào một không gian tên khác trên máy cục bộ của bạn nếu bạn muốn.
Nếu bạn nhớ lại từ <<ch03-git-branching#_remote_branches>>, các điều khiển từ xa thông thường sẽ tìm nạp tất cả các nhánh trên máy chủ từ xa vào `refs/remotes/<remote>/`, như `refs/remotes/origin/master`.
Tuy nhiên, nếu bạn có một số nhánh bạn đang thử nghiệm, bạn có thể bảo Git tìm nạp tất cả các Yêu cầu Kéo và giữ chúng được cập nhật mà không cần phải thêm một loạt các điều khiển từ xa.
Bạn có thể làm điều này bằng cách chỉnh sửa thủ công phần `[remote "origin"]` của tệp `.git/config` của bạn.
Nếu bạn có một tệp cấu hình trông như thế này:
[source,console]
----
[remote "origin"]
url = https://github.com/schacon/blink
fetch = +refs/heads/*:refs/remotes/origin/*
----
Đây chỉ là phần tiêu chuẩn mà `git clone` tạo ra.
Bạn có thể chỉnh sửa nó để thêm một dòng vào đó để bảo Git tìm nạp tất cả các Yêu cầu Kéo và lưu trữ chúng trong một không gian tên khác.
Ví dụ, nếu bạn muốn tìm nạp mọi Yêu cầu Kéo từ kho lưu trữ `schacon/blink` và lưu trữ chúng trong `refs/remotes/origin/pr/*`, bạn có thể thêm dòng này:
[source,console]
----
[remote "origin"]
url = https://github.com/schacon/blink
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/pull/*/head:refs/remotes/origin/pr/*
----
Điều này sẽ bảo Git, "Tìm nạp tất cả các nhánh, và cũng tìm nạp tất cả các Yêu cầu Kéo."
Bây giờ, nếu bạn chạy `git fetch`, bạn sẽ thấy tất cả các nhánh từ xa như bạn đã thấy trước đây, nhưng bạn cũng sẽ thấy tất cả các nhánh cho mỗi Yêu cầu Kéo đang mở.
[source,console]
----
$ git fetch
From https://github.com/schacon/blink
* [new ref] refs/pull/1/head -> origin/pr/1
* [new ref] refs/pull/2/head -> origin/pr/2
...
----
Điều này rất tuyệt, bởi vì bây giờ bạn có thể kiểm tra trực tiếp mã từ một nhánh Yêu cầu Kéo.
[source,console]
----
$ git checkout pr/2
----
Các nhánh mà Git kéo xuống là chỉ đọc, và chúng sẽ được cập nhật mỗi khi bạn `fetch`.
Điều này làm cho việc kiểm tra mã từ một Yêu cầu Kéo cục bộ trở nên siêu dễ dàng.
Tham chiếu `head` trên điều khiển từ xa là nhánh chủ đề của người đóng góp, nhưng cũng có một tham chiếu `merge` đại diện cho trạng thái của mã nếu bạn nhấp vào nút "Merge" trên trang web.
Điều này cho phép bạn kiểm tra việc hợp nhất trước cả khi nhấn nút.
[NOTE]
====
Mẹo này có thể nguy hiểm.
GitHub chỉ có khoảng 400.000 kho lưu trữ có các Yêu cầu Kéo đang mở, nhưng có thể có hàng ngàn Yêu cầu Kéo đang mở trong các dự án bạn quan tâm.
Việc tìm nạp tất cả các Yêu cầu Kéo từ một dự án phổ biến như nhân Linux có thể tải xuống hàng trăm hoặc hàng ngàn nhánh, vì vậy hãy cẩn thận.
====
==== Các Yêu cầu Kéo trên các Yêu cầu Kéo
Bạn không chỉ có thể mở các Yêu cầu Kéo nhắm mục tiêu vào nhánh chính hoặc `master`, mà bạn thực sự có thể mở một Yêu cầu Kéo nhắm mục tiêu vào bất kỳ nhánh nào trong mạng.
Trên thực tế, bạn thậm chí có thể nhắm mục tiêu vào một Yêu cầu Kéo khác.
Đây là một kỹ thuật thực sự hữu ích nếu bạn đang cộng tác với ai đó về một tính năng và bạn có một ý tưởng để cải thiện nhánh tính năng của họ.
Bạn có thể chỉ cần phân nhánh kho lưu trữ của họ, tạo một nhánh nhắm mục tiêu vào cùng một nhánh mà họ đã mở Yêu cầu Kéo, thêm các cam kết của bạn, và sau đó mở một Yêu cầu Kéo mới trở lại nhánh của họ.
Khi bạn mở một Yêu cầu Kéo, có một hộp ở đầu trang chỉ định nhánh nào bạn đang yêu cầu được kéo vào và nhánh nào bạn đang yêu cầu kéo từ đó.
Nếu bạn nhấp vào nút "`Edit`" ở phía bên phải của hộp đó, bạn có thể thay đổi cả nhánh "cơ sở" và "so sánh".
.Thay đổi thủ công nhánh cơ sở của một Yêu cầu Kéo
image::images/pr-base-branch.png[Thay đổi thủ công nhánh cơ sở của một Yêu cầu Kéo]
Bằng cách này, bạn có thể dễ dàng chỉ định rằng nhánh mới của bạn sẽ được hợp nhất vào một Yêu cầu Kéo khác hoặc một nhánh khác của dự án.
==== Đề cập và Thông báo
(((GitHub, notifications)))
Phần khác của khía cạnh mạng xã hội của GitHub mà chúng ta sẽ thảo luận là thông báo.
Bạn có một phần "Notifications" trong cài đặt tài khoản của mình nơi bạn có thể đặt tùy chọn của mình.
.Cài đặt thông báo
image::images/notification-settings.png[Cài đặt thông báo]
Có hai lựa chọn: "Email" và "Web".
Bạn có thể chọn một, cả hai hoặc không chọn cái nào.
===== Thông báo trên Web
Thông báo trên web chỉ tồn tại trên GitHub, và bạn chỉ có thể kiểm tra chúng trên GitHub.
Nếu bạn đã chọn tùy chọn này trong tùy chọn của mình và một thông báo được kích hoạt cho bạn, bạn sẽ thấy một chấm màu xanh nhỏ trên biểu tượng thông báo của mình ở đầu màn hình, như đã thấy trong <<_notifications_icon>>.
[[_notifications_icon]]
.Biểu tượng thông báo
image::images/notifications-icon.png[Biểu tượng thông báo]
Nếu bạn nhấp vào nó, bạn sẽ thấy một danh sách tất cả các mục bạn đã được thông báo, được nhóm theo dự án.
Bạn có thể lọc các thông báo cho một dự án cụ thể bằng cách nhấp vào tên của nó trong thanh bên trái.
Bạn cũng có thể xác nhận một thông báo bằng cách nhấp vào biểu tượng dấu kiểm bên cạnh nó, hoặc xác nhận tất cả các thông báo trong một dự án bằng cách nhấp vào dấu kiểm ở đầu nhóm.
Cũng có một nút "Tắt tiếng" bên cạnh mỗi thông báo sẽ ngăn bạn nhận thêm bất kỳ thông báo nào cho mục đó.
Những công cụ này rất hữu ích để quản lý rất nhiều thông báo.
Nhiều người dùng thành thạo GitHub sẽ chỉ cần tắt hoàn toàn thông báo qua email và quản lý tất cả các thông báo của họ thông qua màn hình này.
===== Thông báo qua Email
Thông báo qua email là cách khác bạn có thể xử lý các thông báo từ GitHub.
Nếu bạn đã bật tính năng này, bạn sẽ nhận được email cho mỗi thông báo.
Chúng ta đã thấy các ví dụ về điều này trong <<_pr_notification_email>> và <<pr-email-resp>>.
Các email cũng sẽ được phân luồng đúng cách, điều này rất hay nếu bạn đang sử dụng một ứng dụng thư khách có phân luồng.
Cũng có rất nhiều siêu dữ liệu được nhúng trong các tiêu đề của các email mà GitHub gửi, điều này có thể rất hữu ích để thiết lập các bộ lọc và quy tắc tùy chỉnh.
Ví dụ, trường `To` của email được gửi khi có một bình luận trên Yêu cầu Kéo #821 trong dự án `schacon/hw` là `schacon/hw <hw@noreply.github.com>`.
Điều này nhất quán cho tất cả các email cho dự án này, vì vậy bạn có thể lọc tất cả email cho dự án này vào một thư mục cụ thể.
Email cũng chứa một tiêu đề `List-Post` chứa một URL.
Bạn có thể sử dụng điều này để đăng lên danh sách (trong trường hợp này, trả lời Yêu cầu Kéo).
Tiêu đề `List-Unsubscribe` cũng có mặt, cung cấp cho bạn một cách dễ dàng để hủy đăng ký khỏi chuỗi.
Nếu bạn không muốn nhận thêm bất kỳ thông báo nào từ Yêu cầu Kéo này, bạn có thể nhấp vào liên kết hủy đăng ký trong tiêu đề.
Để biết thêm thông tin về thông báo qua email, hãy xem các hướng dẫn "Giới thiệu về thông báo qua email" và "Giới thiệu về thông báo trên web" trong tài liệu trợ giúp của GitHub.
==== Các Tệp Đặc biệt
Có một vài tệp đặc biệt mà GitHub sẽ nhận thấy nếu chúng có mặt trong kho lưu trữ của bạn.
===== README
Tệp đầu tiên là tệp `README`, có thể ở hầu hết mọi định dạng mà GitHub nhận dạng là văn xuôi.
Ví dụ, nó có thể là `README`, `README.md`, `README.asciidoc`, v.v.
Nếu GitHub thấy một tệp `README` trong nguồn của bạn, nó sẽ hiển thị nó trên trang đích của kho lưu trữ.
Nhiều nhóm sử dụng tệp này để chứa tất cả thông tin dự án có liên quan cho một người mới làm quen với dự án hoặc kho lưu trữ.
Điều này thường bao gồm những thứ như:
* Dự án dùng để làm gì
* Cách cấu hình và cài đặt nó
* Ví dụ về cách sử dụng nó
* Giấy phép mà dự án được phát hành
* Cách đóng góp vào nó
Vì GitHub sẽ hiển thị tệp này, bạn có thể nhúng hình ảnh hoặc video vào đó để dễ xem.
===== CONTRIBUTING
Tệp đặc biệt khác mà GitHub nhận ra là tệp `CONTRIBUTING`.
Nếu bạn có một tệp có tên `CONTRIBUTING` với bất kỳ phần mở rộng nào (ví dụ: `CONTRIBUTING.md` hoặc `CONTRIBUTING.adoc`), GitHub sẽ hiển thị <<_contributing_file>> khi bạn bắt đầu tạo một Yêu cầu Kéo.
[[_contributing_file]]
.Một liên kết đến tệp CONTRIBUTING khi mở một Yêu cầu Kéo
image::images/contributing-file.png[Một liên kết đến tệp CONTRIBUTING khi mở một Yêu cầu Kéo]
Ý tưởng ở đây là bạn có thể chỉ định những điều cụ thể bạn muốn hoặc không muốn trong một Yêu cầu Kéo được gửi đến dự án của bạn.
Bằng cách này, mọi người có thể thực sự đọc các hướng dẫn đóng góp trước khi mở Yêu cầu Kéo.
==== Quản trị Dự án
Có một vài điều quản trị bạn có thể làm với dự án của mình mà bạn nên biết.
===== Thay đổi Nhánh Mặc định
Một trong số đó là thay đổi nhánh "Mặc định" của kho lưu trữ của bạn.
Đây là nhánh mà GitHub sẽ hiển thị khi ai đó truy cập trang đích kho lưu trữ của bạn, và nó cũng là nhánh mà các Yêu cầu Kéo được mở theo mặc định.
Để thay đổi nhánh mặc định, bạn có thể truy cập trang "Cài đặt" của dự án của bạn và chọn tab "Nhánh".
.Tab "Nhánh" của cài đặt kho lưu trữ
image::images/repo-branches.png[Tab "Nhánh" của cài đặt kho lưu trữ]
Từ đó bạn có thể thay đổi nhánh mặc định.
===== Chuyển giao một Dự án
Bạn cũng có thể chuyển một dự án cho một người dùng khác hoặc một tổ chức nếu bạn muốn.
Điều này hữu ích nếu bạn đang từ bỏ một dự án và ai đó muốn tiếp quản nó, hoặc nếu dự án của bạn trở nên lớn hơn và bạn muốn chuyển nó vào một tổ chức.
Để làm điều này, hãy truy cập trang cài đặt của dự án và nhấp vào nút "Chuyển giao".
.Nút "Chuyển giao quyền sở hữu" trên trang cài đặt của kho lưu trữ
image::images/repo-transfer.png[Nút "Chuyển giao quyền sở hữu" trên trang cài đặt của kho lưu trữ]
[WARNING]
====
Điều này sẽ không chỉ di chuyển kho lưu trữ sang một người dùng hoặc tổ chức khác, mà nó cũng sẽ chuyển hướng tất cả các URL đến vị trí mới.
Nó cũng sẽ chuyển hướng các lần sao chép và tìm nạp từ Git, không chỉ các yêu cầu web.
====
[[ch06-github_orgs]]
=== Quản lý một tổ chức
(((GitHub, organizations)))
Ngoài các tài khoản người dùng đơn lẻ, GitHub còn có cái gọi là Tổ chức (Organizations).
Giống như tài khoản cá nhân, tài khoản Tổ chức có một không gian tên nơi tất cả các dự án của họ tồn tại, nhưng nhiều thứ khác thì khác.
Các tài khoản này đại diện cho một nhóm người có quyền sở hữu chung đối với các dự án, và có nhiều công cụ để quản lý các nhóm con của những người đó.
Thông thường, các tài khoản này được sử dụng cho các nhóm Mã nguồn Mở (như "`perl`" hoặc "`rails`") hoặc các công ty (như "`google`" hoặc "`twitter`").
==== Cơ bản về Tổ chức
Một tổ chức khá dễ tạo; chỉ cần nhấp vào biểu tượng "`+`" ở trên cùng bên phải của bất kỳ trang GitHub nào, và chọn "`New organization`" (Tổ chức mới) từ menu.
.Mục menu "`New organization`"
image::images/neworg.png[Mục menu “New organization”]
Đầu tiên, bạn sẽ cần đặt tên cho tổ chức của mình và cung cấp địa chỉ email cho đầu mối liên hệ chính của nhóm.
Sau đó, bạn có thể mời những người dùng khác làm đồng chủ sở hữu tài khoản nếu bạn muốn.
Làm theo các bước này và bạn sẽ sớm trở thành chủ sở hữu của một tổ chức hoàn toàn mới.
Giống như tài khoản cá nhân, các tổ chức là miễn phí nếu mọi thứ bạn dự định lưu trữ ở đó đều là mã nguồn mở.
Là chủ sở hữu trong một tổ chức, khi bạn fork một kho lưu trữ, bạn sẽ có lựa chọn fork nó vào không gian tên của tổ chức của mình.
Khi bạn tạo kho lưu trữ mới, bạn có thể tạo chúng dưới tài khoản cá nhân của mình hoặc dưới bất kỳ tổ chức nào mà bạn là chủ sở hữu.
Bạn cũng tự động "`watch`" (theo dõi) bất kỳ kho lưu trữ mới nào được tạo dưới các tổ chức này.
Giống như trong <<_personal_avatar>>, bạn có thể tải lên hình đại diện cho tổ chức của mình để cá nhân hóa nó một chút.
Cũng giống như tài khoản cá nhân, bạn có một trang đích cho tổ chức liệt kê tất cả các kho lưu trữ của bạn và có thể được xem bởi những người khác.
Bây giờ hãy đề cập đến một số điều hơi khác với tài khoản tổ chức.
==== Các Nhóm (Teams)
Các tổ chức được liên kết với các cá nhân thông qua các nhóm (teams), đơn giản là một nhóm các tài khoản người dùng cá nhân và kho lưu trữ trong tổ chức và loại quyền truy cập mà những người đó có trong các kho lưu trữ đó.
Ví dụ, giả sử công ty của bạn có ba kho lưu trữ: `frontend`, `backend`, và `deployscripts`.
Bạn muốn các nhà phát triển HTML/CSS/JavaScript của mình có quyền truy cập vào `frontend` và có thể là `backend`, và những người Vận hành của bạn có quyền truy cập vào `backend` và `deployscripts`.
Các nhóm làm cho điều này trở nên dễ dàng, mà không cần phải quản lý các cộng tác viên cho từng kho lưu trữ riêng lẻ.
Trang Tổ chức hiển thị cho bạn một bảng điều khiển đơn giản về tất cả các kho lưu trữ, người dùng và nhóm thuộc tổ chức này.
[[_org_page]]
.Trang Tổ chức
image::images/orgs-01-page.png[Trang Tổ chức]
Để quản lý các Nhóm của bạn, bạn có thể nhấp vào thanh bên Teams ở phía bên phải của trang trong <<_org_page>>.
Điều này sẽ đưa bạn đến một trang bạn có thể sử dụng để thêm thành viên vào nhóm, thêm kho lưu trữ vào nhóm hoặc quản lý cài đặt và cấp độ kiểm soát truy cập cho nhóm.
Mỗi nhóm có thể có quyền truy cập chỉ đọc, đọc/ghi hoặc quản trị đối với các kho lưu trữ.
Bạn có thể thay đổi cấp độ đó bằng cách nhấp vào nút "`Settings`" trong <<_team_page>>.
[[_team_page]]
.Trang Nhóm
image::images/orgs-02-teams.png[Trang Nhóm]
Khi bạn mời ai đó vào một nhóm, họ sẽ nhận được email cho họ biết họ đã được mời.
Ngoài ra, `@mentions` nhóm (như `@acmecorp/frontend`) hoạt động giống như cách chúng làm với người dùng cá nhân, ngoại trừ việc *tất cả* thành viên của nhóm sau đó đều đăng ký vào luồng.
Điều này hữu ích nếu bạn muốn sự chú ý từ ai đó trong một nhóm, nhưng bạn không biết chính xác ai để hỏi.
Một người dùng có thể thuộc về bất kỳ số lượng nhóm nào, vì vậy đừng giới hạn bản thân chỉ trong các nhóm kiểm soát truy cập.
Các nhóm quan tâm đặc biệt như `ux`, `css`, hoặc `refactoring` hữu ích cho một số loại câu hỏi nhất định, và những nhóm khác như `legal` và `colorblind` cho một loại hoàn toàn khác.
==== Nhật ký Kiểm toán (Audit Log)
Các tổ chức cũng cung cấp cho chủ sở hữu quyền truy cập vào tất cả thông tin về những gì đã diễn ra dưới tổ chức.
Bạn có thể chuyển đến tab 'Audit Log' và xem những sự kiện nào đã xảy ra ở cấp độ tổ chức, ai đã thực hiện chúng và chúng được thực hiện ở đâu trên thế giới.
[[_the_audit_log]]
.Nhật ký Kiểm toán
image::images/orgs-03-audit.png[Nhật ký Kiểm toán]
Bạn cũng có thể lọc xuống các loại sự kiện cụ thể, địa điểm cụ thể hoặc những người cụ thể.
[[_scripting_github]]
=== Lập trình kịch bản cho GitHub
Đến đây, chúng ta đã đề cập đến tất cả các tính năng và quy trình làm việc chính của GitHub, nhưng bất kỳ nhóm hoặc dự án lớn nào cũng sẽ có các tùy chỉnh mà họ có thể muốn thực hiện hoặc các dịch vụ bên ngoài mà họ có thể muốn tích hợp.
May mắn cho chúng ta, GitHub thực sự khá dễ dàng để "hack" theo nhiều cách.
Trong phần này, chúng ta sẽ đề cập đến cách sử dụng hệ thống hook của GitHub và API của nó để làm cho GitHub hoạt động theo cách chúng ta muốn.
==== Các Dịch vụ và Hook
Phần Hook và Dịch vụ trong quản trị kho lưu trữ GitHub là cách dễ nhất để GitHub tương tác với các hệ thống bên ngoài.
===== Các Dịch vụ
Đầu tiên, hãy xem xét các Dịch vụ.
Cả hai tích hợp Hook và Dịch vụ đều có thể được tìm thấy trong phần Cài đặt của kho lưu trữ của bạn, dưới tab Webhook và Dịch vụ.
.Tab Webhook & Dịch vụ
image::images/services-and-hooks-tab.png[Tab Webhook & Dịch vụ]
Có hàng tá dịch vụ bạn có thể lựa chọn, hầu hết trong số đó là tích hợp vào các hệ thống thương mại và mã nguồn mở khác.
Hầu hết chúng là dành cho các dịch vụ Tích hợp Liên tục, các trình theo dõi lỗi và vấn đề, các hệ thống phòng trò chuyện, và các hệ thống tài liệu.
Chúng ta sẽ đi qua việc thiết lập một cái rất đơn giản, hook Email.
Nếu bạn chọn "Email" từ trình đơn thả xuống "Thêm dịch vụ", bạn sẽ nhận được một màn hình cấu hình giống như trong <<_email_hook_config>>.
[[_email_hook_config]]
.Cấu hình hook email
image::images/email-hook-config.png[Cấu hình hook email]
Trong trường hợp này, nếu chúng ta nhấp vào nút "Thêm dịch vụ", một email sẽ được gửi đến địa chỉ chúng ta đã cung cấp mỗi khi ai đó đẩy vào kho lưu trữ.
Các dịch vụ có thể được sử dụng để lắng nghe nhiều loại sự kiện khác nhau, nhưng hầu hết chúng chỉ lắng nghe các sự kiện đẩy và sau đó làm gì đó với dữ liệu đó.
Nếu bạn có một hệ thống bạn muốn tích hợp với GitHub, nhưng nó không được liệt kê trong danh sách này, bạn có một lựa chọn khác: webhook.
===== Webhook
Webhook là một loại tích hợp Dịch vụ đặc biệt có khả năng tùy chỉnh cao.
Chúng là một cách rất đơn giản để GitHub thực hiện một POST HTTP đến một URL bất cứ khi nào một sự kiện nhất định xảy ra.
Để thiết lập một webhook, bạn nhấp vào nút "Thêm webhook" trong phần Webhook của trang cài đặt "Webhook & dịch vụ".
Điều này sẽ hiển thị một biểu mẫu giống như trong <<_webhook_form>>.
[[_webhook_form]]
.Biểu mẫu cấu hình webhook
image::images/webhook-form.png[Biểu mẫu cấu hình webhook]
Việc thiết lập khá đơn giản.
Bạn cung cấp một URL mà bạn muốn GitHub POST đến, và bạn có thể chọn một bí mật sẽ được gửi cùng với POST, mà bạn có thể sử dụng để xác minh rằng POST đến từ GitHub.
Bạn cũng có thể chọn từ một số sự kiện khác nhau sẽ kích hoạt webhook.
Mặc định là chỉ được kích hoạt bởi một sự kiện `push` -- khi ai đó đẩy mã mới vào bất kỳ nhánh nào của kho lưu trữ.
Bạn có thể để nó được kích hoạt khi một kho lưu trữ được phân nhánh (`fork`), một nhánh hoặc thẻ mới được tạo (`create`), một nhánh hoặc thẻ bị xóa (`delete`), một yêu cầu kéo được mở (`pull_request`), hoặc một vấn đề mới được mở (`issues`).
Hoặc, bạn có thể để nó kích hoạt cho mọi thứ.
Nếu chúng ta nhấp vào "Thêm webhook", GitHub sẽ POST đến URL chúng ta đã cung cấp với một số thông tin về sự kiện đã kích hoạt nó.
==== API GitHub
Cách cuối cùng chúng ta có thể làm cho GitHub hoạt động với các hệ thống bên ngoài là thông qua API của nó.
GitHub có một số API, nhưng cho mục đích của chúng ta, chúng ta sẽ tập trung vào API chính, hiện đang là phiên bản 3.
Bạn có thể tìm thấy tài liệu đầy đủ tại https://docs.github.com/rest/[^].
===== Những điều Cơ bản
Tất cả quyền truy cập API đều qua HTTPS, và tất cả dữ liệu được gửi và nhận dưới dạng JSON.
Mọi yêu cầu API đều yêu cầu một tiêu đề `Accept`.
[source,console]
----
$ curl -i https://api.github.com/users/schacon
HTTP/1.1 200 OK
Server: GitHub.com
Date: Sun, 11 May 2014 15:43:16 GMT
Content-Type: application/json; charset=utf-8
Status: 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1400000000
{
"login": "schacon",
"id": 70,
...
}
----
Nếu chúng ta không chỉ định một tiêu đề `Accept`, chúng ta sẽ nhận lại rất nhiều văn bản, và tiêu đề `Content-Type` sẽ là `text/html`.
Nếu chúng ta chỉ định `application/vnd.github.v3+json` làm giá trị cho tiêu đề `Accept`, chúng ta sẽ nhận lại một đối tượng JSON.
[source,console]
----
$ curl -i -H "Accept: application/vnd.github.v3+json" \
https://api.github.com/users/schacon
----
Chúng ta sẽ sử dụng tiêu đề `Accept` này trong tất cả các yêu cầu API của mình.
Điều khác bạn sẽ cần là một mã thông báo OAuth để xác thực.
Có một số cách để có được một mã thông báo, nhưng đối với các kịch bản đơn giản, cách dễ nhất là tạo một Mã thông báo Truy cập Cá nhân.
Bạn có thể làm điều này từ tab "Mã thông báo truy cập cá nhân" trong trang cài đặt của bạn.
.Tab mã thông báo truy cập cá nhân
image::images/personal-access-tokens.png[Tab mã thông báo truy cập cá nhân]
Điều này sẽ yêu cầu bạn mô tả về mã thông báo và chọn "phạm vi" cho mã thông báo.
Các phạm vi xác định những gì bạn có thể làm với mã thông báo.
Một ý tưởng hay là giới hạn phạm vi chỉ trong những gì bạn cần.
Ví dụ, nếu bạn chỉ muốn đọc dữ liệu người dùng và các kho lưu trữ, bạn có thể chọn các phạm vi `user:email` và `repo`.
[[_auth_token_scopes]]
.Các phạm vi cho một mã thông báo truy cập cá nhân mới
image::images/auth-token-scopes.png[Các phạm vi cho một mã thông báo truy cập cá nhân mới]
Khi bạn đã có mã thông báo, bạn có thể sử dụng nó để xác thực với API bằng cách chuyển nó trong tiêu đề `Authorization`.
[source,console]
----
$ curl -i -H "Authorization: token <token>" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/user
----
Tại thời điểm này, bạn sẽ có thể thực hiện các yêu cầu API cho dữ liệu người dùng của riêng mình.
Hãy thử lấy dữ liệu người dùng của bạn.
[source,console]
----
$ curl -i -H "Authorization: token <token>" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/user
----
Bây giờ hãy thử làm một cái gì đó thú vị hơn một chút.
Hãy xem chúng ta có thể tạo một vấn đề trong một kho lưu trữ hay không.
Để làm điều này, chúng ta sẽ cần biết chủ sở hữu của kho lưu trữ và tên kho lưu trữ.
Hãy thử tạo một vấn đề trong kho lưu trữ `schacon/blink`.
Chúng ta sẽ cần thực hiện một yêu cầu `POST` đến `/repos/<user>/<repo>/issues/<num>/comments` với một đối tượng JSON chứa tiêu đề và nội dung của vấn đề.
[source,console]
----
$ curl -i -H "Authorization: token <token>" \
-H "Accept: application/vnd.github.v3+json" \
-d '{"title": "Test issue", "body": "This is a test"}' \
https://api.github.com/repos/schacon/blink/issues
----
Chúng ta cũng có thể sử dụng API để bình luận về vấn đề này.
Để làm điều này, chúng ta sẽ cần thực hiện một yêu cầu `POST` đến `/repos/:owner/:repo/issues/:number/comments` với một đối tượng JSON chứa nội dung của bình luận.
[source,console]
----
$ curl -i -H "Authorization: token <token>" \
-H "Accept: application/vnd.github.v3+json" \
-d '{"body": "This is a test comment"}' \
https://api.github.com/repos/schacon/blink/issues/3/comments
----
Chúng ta có thể thấy rằng bình luận đã được tạo thành công.
Bạn có thể sử dụng API để làm những việc như:
* Liệt kê các cam kết
* Tạo và chỉnh sửa các nhãn
* Tạo, chỉnh sửa, và xóa các kho lưu trữ
* Thêm và xóa các cộng tác viên
* và nhiều hơn nữa
Đây chỉ là một mẫu nhỏ của những gì có sẵn.
Bạn có thể tìm thấy tài liệu đầy đủ cho API tại https://docs.github.com/rest/[^].
===== Ví dụ: Một Dịch vụ Trạng thái Cam kết
API GitHub khá rộng lớn, và bạn có thể nhận được gần như bất kỳ dữ liệu nào bạn muốn về các kho lưu trữ của mình.
Trong phần này, chúng ta sẽ viết một dịch vụ nhỏ sẽ lắng nghe các sự kiện đẩy và sau đó đăng một trạng thái lên cam kết cho bạn biết cam kết có "hợp lệ" hay không.
Để làm điều này, trước tiên chúng ta sẽ cần thiết lập một webhook.
Hãy truy cập phần "Webhook" trong cài đặt kho lưu trữ của chúng ta và nhấp vào "Thêm webhook".
Chúng ta sẽ cung cấp một URL cho dịch vụ của mình, và một bí mật.
Chúng ta cũng sẽ chọn chỉ nhận các sự kiện `push`.
Bây giờ, dịch vụ của chúng ta sẽ nhận được một yêu cầu `POST` mỗi khi ai đó đẩy vào kho lưu trữ.
Hãy viết một dịch vụ Sinatra đơn giản để xử lý điều này.
[source,ruby]
----
require 'sinatra'
require 'json'
require 'git'
require 'octokit'
post '/payload' do
push = JSON.parse(request.body.read)
repo = push['repository']['full_name']
sha = push['after']
client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
g = Git.open(ENV['GIT_REPO_PATH'])
commit = g.gcommit(sha)
if commit.message.include?('LGTM')
client.create_status(repo, sha, 'success')
else
client.create_status(repo, sha, 'failure')
end
end
----
Đây là một dịch vụ khá đơn giản.
Nó lấy tên kho lưu trữ và SHA-1 của cam kết đã được đẩy.
Sau đó, nó sử dụng gem `octokit` (https://github.com/octokit/octokit.rb[^]) để tạo một trạng thái trên cam kết.
Trạng thái có thể là một trong `pending`, `success`, `error`, hoặc `failure`.
Cuối cùng, nó kiểm tra xem thông điệp cam kết có chứa "LGTM" hay không và đặt trạng thái tương ứng.
Và đó là tất cả!
Bây giờ, mỗi khi ai đó đẩy vào kho lưu trữ của chúng ta, chúng ta sẽ nhận được một trạng thái trên cam kết cho chúng ta biết thông điệp cam kết có hợp lệ hay không.
Bạn có thể xem một ví dụ về điều này trong <<_commit_status>>.
[[_commit_status]]
.Trạng thái cam kết
image::images/commit-status.png[Trạng thái cam kết]
Bạn có thể thấy dấu kiểm màu xanh lá cây nhỏ bên cạnh SHA-1 cam kết.
Đây là một tính năng rất mạnh mẽ để tích hợp các dịch vụ bên ngoài với GitHub.
=== Tóm tắt
Bây giờ bạn đã là người dùng GitHub.
Bạn biết cách tạo tài khoản, quản lý tổ chức, tạo và đẩy lên kho, đóng góp vào dự án khác và nhận đóng góp từ họ.
Trong chương tiếp theo, bạn sẽ học các công cụ mạnh mẽ hơn và mẹo để xử lý các tình huống phức tạp, giúp bạn thành thạo Git hơn.
[[ch07-git-tools]]
== Công cụ Git
Đến lúc này, bạn đã học được hầu hết các lệnh và luồng làm việc hàng ngày cần thiết để quản lý hoặc duy trì một kho Git cho việc kiểm soát mã nguồn.
Bạn đã hoàn thành các tác vụ cơ bản như theo dõi và commit tệp, và tận dụng sức mạnh của vùng dàn (staging area) cùng với nhánh nhẹ để phát triển tính năng và hợp nhất.
Bây giờ bạn sẽ khám phá một số tính năng rất mạnh của Git mà có thể bạn không dùng hàng ngày nhưng có thể cần đến vào một lúc nào đó.
[[_revision_selection]]
=== Chọn Phiên bản (Revision Selection)
Git cho phép bạn tham chiếu đến một commit đơn lẻ, một tập hợp các commit, hoặc một khoảng các commit theo nhiều cách.
Chúng không nhất thiết phải rõ ràng nhưng rất hữu ích để biết.
==== Các Phiên bản Đơn lẻ
Bạn rõ ràng có thể tham chiếu đến bất kỳ commit đơn lẻ nào bằng mã băm SHA-1 đầy đủ, 40 ký tự của nó, nhưng cũng có những cách thân thiện với con người hơn để tham chiếu đến các commit.
Phần này phác thảo các cách khác nhau mà bạn có thể tham chiếu đến bất kỳ commit nào.
==== SHA-1 Ngắn
Git đủ thông minh để tìm ra commit nào bạn đang tham chiếu đến nếu bạn cung cấp một vài ký tự đầu tiên của mã băm SHA-1, miễn là mã băm một phần đó dài ít nhất bốn ký tự và không mơ hồ; nghĩa là, không có đối tượng nào khác trong cơ sở dữ liệu đối tượng có mã băm bắt đầu bằng cùng một tiền tố.
Ví dụ, để kiểm tra một commit cụ thể mà bạn biết bạn đã thêm chức năng nhất định, bạn có thể chạy lệnh `git log` trước để xác định vị trí commit:
[source,console]
----
$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <schacon@gmail.com>
Date: Fri Jan 2 18:32:33 2009 -0800
Fix refs handling, add gc auto, update tests
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date: Thu Dec 11 15:08:43 2008 -0800
Merge commit 'phedders/rdocs'
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date: Thu Dec 11 14:58:32 2008 -0800
Add some blame and merge stuff
----
Trong trường hợp này, giả sử bạn quan tâm đến commit có mã băm bắt đầu bằng `1c002dd...`.
Bạn có thể kiểm tra commit đó với bất kỳ biến thể nào sau đây của `git show` (giả sử các phiên bản ngắn hơn là không mơ hồ):
[source,console]
----
$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b
$ git show 1c002dd4b536e7479f
$ git show 1c002d
----
Git có thể tìm ra một từ viết tắt ngắn, duy nhất cho các giá trị SHA-1 của bạn.
Nếu bạn truyền `--abbrev-commit` cho lệnh `git log`, đầu ra sẽ sử dụng các giá trị ngắn hơn nhưng giữ cho chúng là duy nhất; nó mặc định sử dụng bảy ký tự nhưng làm cho chúng dài hơn nếu cần thiết để giữ cho SHA-1 không bị mơ hồ:
[source,console]
----
$ git log --abbrev-commit --pretty=oneline
ca82a6d Change the version number
085bb3b Remove unnecessary test code
a11bef0 Initial commit
----
Nói chung, tám đến mười ký tự là quá đủ để trở nên duy nhất trong một dự án.
Ví dụ, tính đến tháng 2 năm 2019, hạt nhân Linux (là một dự án khá lớn) có hơn 875.000 commit và gần bảy triệu đối tượng trong cơ sở dữ liệu đối tượng của nó, mà không có hai đối tượng nào có SHA-1 giống hệt nhau trong 12 ký tự đầu tiên.
[NOTE]
.MỘT LƯU Ý NGẮN VỀ SHA-1
====
Nhiều người trở nên lo lắng tại một thời điểm nào đó rằng họ sẽ, do ngẫu nhiên, có hai đối tượng riêng biệt trong kho lưu trữ của họ có cùng giá trị băm SHA-1.
Vậy thì sao?
Nếu bạn thực sự commit một đối tượng có cùng giá trị băm SHA-1 với một đối tượng _khác_ trước đó trong kho lưu trữ của bạn, Git sẽ thấy đối tượng trước đó đã có trong cơ sở dữ liệu Git của bạn, cho rằng nó đã được ghi và đơn giản là sử dụng lại nó.
Nếu bạn cố gắng checkout đối tượng đó một lần nữa tại một thời điểm nào đó, bạn sẽ luôn nhận được dữ liệu của đối tượng đầu tiên.
Tuy nhiên, bạn nên biết kịch bản này khó xảy ra đến mức nực cười như thế nào.
Bản tóm tắt SHA-1 dài 20 byte hoặc 160 bit.
Số lượng đối tượng được băm ngẫu nhiên cần thiết để đảm bảo xác suất 50% của một va chạm đơn lẻ là khoảng 2^80^ (công thức để xác định xác suất va chạm là `p = (n(n-1)/2) * (1/2^160)`).
2^80^ là 1,2 x 10^24^ hoặc 1 triệu tỷ tỷ.
Đó là gấp 1.200 lần số lượng hạt cát trên trái đất.
Đây là một ví dụ để cung cấp cho bạn một ý tưởng về những gì cần thiết để có được một va chạm SHA-1.
Nếu tất cả 6,5 tỷ người trên Trái đất đều đang lập trình, và mỗi giây, mỗi người đều tạo ra mã tương đương với toàn bộ lịch sử hạt nhân Linux (6,5 triệu đối tượng Git) và đẩy nó vào một kho lưu trữ Git khổng lồ, sẽ mất khoảng 2 năm cho đến khi kho lưu trữ đó chứa đủ các đối tượng để có xác suất 50% của một va chạm đối tượng SHA-1 đơn lẻ.
Do đó, một va chạm SHA-1 hữu cơ ít có khả năng xảy ra hơn việc mọi thành viên trong nhóm lập trình của bạn bị tấn công và giết bởi những con sói trong các sự cố không liên quan vào cùng một đêm.
Nếu bạn dành vài nghìn đô la sức mạnh tính toán cho nó, có thể tổng hợp hai tệp có cùng mã băm, như đã được chứng minh trên https://shattered.io/[^] vào tháng 2 năm 2017.
Git đang chuyển sang sử dụng SHA256 làm thuật toán băm mặc định, thuật toán này có khả năng chống lại các cuộc tấn công va chạm tốt hơn nhiều, và có mã tại chỗ để giúp giảm thiểu cuộc tấn công này (mặc dù nó không thể loại bỏ hoàn toàn nó).
====
[[_branch_references]]
==== Tham chiếu Nhánh
Một cách đơn giản để tham chiếu đến một commit cụ thể là nếu nó là commit ở đầu của một nhánh; trong trường hợp đó, bạn có thể đơn giản sử dụng tên nhánh trong bất kỳ lệnh Git nào mong đợi một tham chiếu đến một commit.
Ví dụ, nếu bạn muốn kiểm tra đối tượng commit cuối cùng trên một nhánh, các lệnh sau là tương đương, giả sử rằng nhánh `topic1` trỏ đến commit `ca82a6d...`:
[source,console]
----
$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1
----
Nếu bạn muốn xem SHA-1 cụ thể nào mà một nhánh trỏ đến, hoặc nếu bạn muốn xem bất kỳ ví dụ nào trong số này rút gọn lại thành gì về mặt SHA-1, bạn có thể sử dụng một công cụ cấp thấp (plumbing tool) của Git gọi là `rev-parse`.
Bạn có thể xem <<ch10-git-internals#ch10-git-internals>> để biết thêm thông tin về các công cụ cấp thấp; về cơ bản, `rev-parse` tồn tại cho các hoạt động cấp thấp hơn và không được thiết kế để sử dụng trong các hoạt động hàng ngày.
Tuy nhiên, đôi khi nó có thể hữu ích khi bạn cần xem những gì thực sự đang diễn ra.
Ở đây bạn có thể chạy `rev-parse` trên nhánh của mình.
[source,console]
----
$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949
----
[[_git_reflog]]
==== Tên ngắn RefLog
Một trong những điều Git làm trong nền trong khi bạn đang làm việc là giữ một "`reflog`" -- một nhật ký về nơi các tham chiếu HEAD và nhánh của bạn đã ở trong vài tháng qua.
Bạn có thể xem reflog của mình bằng cách sử dụng `git reflog`:
[source,console]
----
$ git reflog
734713b HEAD@{0}: commit: Fix refs handling, add gc auto, update tests
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by the 'recursive' strategy.
1c002dd HEAD@{2}: commit: Add some blame and merge stuff
1c36188 HEAD@{3}: rebase -i (squash): updating HEAD
95df984 HEAD@{4}: commit: # This is a combination of two commits.
1c36188 HEAD@{5}: rebase -i (squash): updating HEAD
7e05da5 HEAD@{6}: rebase -i (pick): updating HEAD
----
Mỗi khi đầu nhánh của bạn được cập nhật vì bất kỳ lý do gì, Git lưu trữ thông tin đó cho bạn trong lịch sử tạm thời này.
Bạn cũng có thể sử dụng dữ liệu reflog của mình để tham chiếu đến các commit cũ hơn.
Ví dụ, nếu bạn muốn xem giá trị thứ năm trước đó của HEAD của kho lưu trữ của bạn, bạn có thể sử dụng tham chiếu `@{5}` mà bạn thấy trong đầu ra reflog:
[source,console]
----
$ git show HEAD@{5}
----
Bạn cũng có thể sử dụng cú pháp này để xem một nhánh đã ở đâu một khoảng thời gian cụ thể trước đây.
Ví dụ, để xem nhánh `master` của bạn đã ở đâu ngày hôm qua, bạn có thể gõ:
[source,console]
----
$ git show master@{yesterday}
----
Điều đó sẽ cho bạn thấy đầu của nhánh `master` của bạn đã ở đâu ngày hôm qua.
Kỹ thuật này chỉ hoạt động đối với dữ liệu vẫn còn trong reflog của bạn, vì vậy bạn không thể sử dụng nó để tìm các commit cũ hơn vài tháng.
Để xem thông tin reflog được định dạng giống như đầu ra `git log`, bạn có thể chạy `git log -g`:
[source,console]
----
$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: commit: Fix refs handling, add gc auto, update tests
Author: Scott Chacon <schacon@gmail.com>
Date: Fri Jan 2 18:32:33 2009 -0800
Fix refs handling, add gc auto, update tests
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Reflog: master@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: merge phedders/rdocs: Merge made by recursive.
Author: Scott Chacon <schacon@gmail.com>
Date: Thu Dec 11 15:08:43 2008 -0800
Merge commit 'phedders/rdocs'
----
Điều quan trọng cần lưu ý là thông tin reflog hoàn toàn là cục bộ -- nó là một nhật ký chỉ về những gì _bạn_ đã làm trong kho lưu trữ _của bạn_.
Các tham chiếu sẽ không giống nhau trên bản sao kho lưu trữ của người khác; ngoài ra, ngay sau khi bạn sao chép (clone) một kho lưu trữ ban đầu, bạn sẽ có một reflog trống, vì chưa có hoạt động nào xảy ra trong kho lưu trữ của bạn.
Chạy `git show HEAD@{2.months.ago}` sẽ chỉ hiển thị cho bạn commit phù hợp nếu bạn đã sao chép dự án ít nhất hai tháng trước -- nếu bạn sao chép nó gần đây hơn thế, bạn sẽ chỉ thấy commit cục bộ đầu tiên của mình.
[TIP]
.Hãy nghĩ về reflog như phiên bản lịch sử shell của Git
====
Nếu bạn có nền tảng UNIX hoặc Linux, bạn có thể nghĩ về reflog như phiên bản lịch sử shell của Git, điều này nhấn mạnh rằng những gì ở đó rõ ràng chỉ liên quan đến bạn và "`phiên làm việc`" của bạn, và không liên quan gì đến bất kỳ ai khác có thể đang làm việc trên cùng một máy.
====
[NOTE]
.Thoát các dấu ngoặc nhọn trong PowerShell
====
Khi sử dụng PowerShell, các dấu ngoặc nhọn như `{` và `}` là các ký tự đặc biệt và phải được thoát (escape).
Bạn có thể thoát chúng bằng một dấu huyền (backtick) ` hoặc đặt tham chiếu commit trong dấu ngoặc kép:
[source,console]
----
$ git show HEAD@{0} # sẽ KHÔNG hoạt động
$ git show HEAD@`{0`} # OK
$ git show "HEAD@{0}" # OK
----
====
==== Tham chiếu Tổ tiên
Cách chính khác để chỉ định một commit là thông qua tổ tiên của nó.
Nếu bạn đặt một dấu mũ `^` (caret) ở cuối một tham chiếu, Git giải quyết nó có nghĩa là cha mẹ của commit đó.
Giả sử bạn xem lịch sử của dự án của mình:
[source,console]
----
$ git log --pretty=format:'%h %s' --graph
* 734713b Fix refs handling, add gc auto, update tests
* d921970 Merge commit 'phedders/rdocs'
|\
| * 35cfb2b Some rdoc changes
* | 1c002dd Add some blame and merge stuff
|/
* 1c36188 Ignore *.gem
* 9b29157 Add open3_detach to gemspec file list
----
Sau đó, bạn có thể xem commit trước đó bằng cách chỉ định `HEAD^`, có nghĩa là "`cha mẹ của HEAD`":
[source,console]
----
$ git show HEAD^
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date: Thu Dec 11 15:08:43 2008 -0800
Merge commit 'phedders/rdocs'
----
[NOTE]
.Thoát dấu mũ trên Windows
====
Trên Windows trong `cmd.exe`, `^` là một ký tự đặc biệt và cần được xử lý khác.
Bạn có thể nhân đôi nó hoặc đặt tham chiếu commit trong dấu ngoặc kép:
[source,console]
----
$ git show HEAD^ # sẽ KHÔNG hoạt động trên Windows
$ git show HEAD^^ # OK
$ git show "HEAD^" # OK
----
====
Bạn cũng có thể chỉ định một số sau `^` để xác định cha mẹ _nào_ bạn muốn; ví dụ, `d921970^2` có nghĩa là "`cha mẹ thứ hai của d921970.`"
Cú pháp này chỉ hữu ích cho các commit hợp nhất (merge commit), có nhiều hơn một cha mẹ -- cha mẹ _đầu tiên_ của một commit hợp nhất là từ nhánh bạn đang ở khi bạn hợp nhất (thường là `master`), trong khi cha mẹ _thứ hai_ của một commit hợp nhất là từ nhánh đã được hợp nhất (ví dụ, `topic`):
[source,console]
----
$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date: Thu Dec 11 14:58:32 2008 -0800
Add some blame and merge stuff
$ git show d921970^2
commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548
Author: Paul Hedderly <paul+git@mjr.org>
Date: Wed Dec 10 22:22:03 2008 +0000
Some rdoc changes
----
Đặc tả tổ tiên chính khác là dấu ngã `~` (tilde).
Điều này cũng tham chiếu đến cha mẹ đầu tiên, vì vậy `HEAD~` và `HEAD^` là tương đương.
Sự khác biệt trở nên rõ ràng khi bạn chỉ định một số.
`HEAD~2` có nghĩa là "`cha mẹ đầu tiên của cha mẹ đầu tiên,`" hoặc "`ông bà`" -- nó duyệt qua các cha mẹ đầu tiên số lần bạn chỉ định.
Ví dụ, trong lịch sử được liệt kê trước đó, `HEAD~3` sẽ là:
[source,console]
----
$ git show HEAD~3
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date: Fri Nov 7 13:47:59 2008 -0500
Ignore *.gem
----
Điều này cũng có thể được viết là `HEAD\~~~`, một lần nữa là cha mẹ đầu tiên của cha mẹ đầu tiên của cha mẹ đầu tiên:
[source,console]
----
$ git show HEAD~~~
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date: Fri Nov 7 13:47:59 2008 -0500
Ignore *.gem
----
Bạn cũng có thể kết hợp các cú pháp này -- bạn có thể lấy cha mẹ thứ hai của tham chiếu trước đó (giả sử đó là một commit hợp nhất) bằng cách sử dụng `HEAD~3^2`, v.v.
[[_commit_ranges]]
==== Khoảng Commit
Bây giờ bạn có thể chỉ định các commit riêng lẻ, hãy xem cách chỉ định các khoảng commit.
Điều này đặc biệt hữu ích để quản lý các nhánh của bạn -- nếu bạn có nhiều nhánh, bạn có thể sử dụng các đặc tả khoảng để trả lời các câu hỏi như, "`Công việc nào đang ở trên nhánh này mà tôi chưa hợp nhất vào nhánh chính của mình?`"
===== Hai Chấm (Double Dot)
Đặc tả khoảng phổ biến nhất là cú pháp hai chấm.
Điều này về cơ bản yêu cầu Git giải quyết một khoảng các commit có thể truy cập được từ một commit nhưng không thể truy cập được từ một commit khác.
Ví dụ, giả sử bạn có một lịch sử commit trông giống như <<double_dot>>.
[[double_dot]]
.Lịch sử ví dụ cho việc chọn khoảng
image::images/double-dot.png[Lịch sử ví dụ cho việc chọn khoảng]
Giả sử bạn muốn xem những gì có trong nhánh `experiment` của mình mà chưa được hợp nhất vào nhánh `master` của bạn.
Bạn có thể yêu cầu Git hiển thị cho bạn một nhật ký chỉ các commit đó với `master..experiment` -- điều đó có nghĩa là "`tất cả các commit có thể truy cập được từ `experiment` mà không thể truy cập được từ `master`.`"
Vì mục đích ngắn gọn và rõ ràng trong các ví dụ này, các chữ cái của các đối tượng commit từ sơ đồ được sử dụng thay cho đầu ra nhật ký thực tế theo thứ tự mà chúng sẽ hiển thị:
[source,console]
----
$ git log master..experiment
D
C
----
Mặt khác, nếu bạn muốn xem điều ngược lại -- tất cả các commit trong `master` mà không có trong `experiment` -- bạn có thể đảo ngược tên các nhánh.
`experiment..master` hiển thị cho bạn mọi thứ trong `master` không thể truy cập được từ `experiment`:
[source,console]
----
$ git log experiment..master
F
E
----
Điều này hữu ích nếu bạn muốn giữ cho nhánh `experiment` được cập nhật và xem trước những gì bạn sắp hợp nhất.
Một cách sử dụng thường xuyên khác của cú pháp này là để xem những gì bạn sắp đẩy lên một điều khiển từ xa (remote):
[source,console]
----
$ git log origin/master..HEAD
----
Lệnh này hiển thị cho bạn bất kỳ commit nào trong nhánh hiện tại của bạn mà không có trong nhánh `master` trên điều khiển từ xa `origin` của bạn.
Nếu bạn chạy `git push` và nhánh hiện tại của bạn đang theo dõi `origin/master`, các commit được liệt kê bởi `git log origin/master..HEAD` là các commit sẽ được chuyển đến máy chủ.
Bạn cũng có thể bỏ qua một bên của cú pháp để Git giả định là `HEAD`.
Ví dụ, bạn có thể nhận được kết quả tương tự như trong ví dụ trước bằng cách gõ `git log origin/master..` -- Git thay thế `HEAD` nếu thiếu một bên.
===== Nhiều Điểm
Cú pháp hai chấm rất hữu ích như một cách viết tắt, nhưng có lẽ bạn muốn chỉ định nhiều hơn hai nhánh để chỉ định phiên bản của mình, chẳng hạn như xem những commit nào có trong bất kỳ nhánh nào trong số nhiều nhánh mà không có trong nhánh bạn đang ở.
Git cho phép bạn làm điều này bằng cách sử dụng ký tự `^` hoặc `--not` trước bất kỳ tham chiếu nào mà từ đó bạn không muốn xem các commit có thể truy cập được.
Do đó, ba lệnh sau là tương đương:
[source,console]
----
$ git log refA..refB
$ git log ^refA refB
$ git log refB --not refA
----
Điều này rất hay vì với cú pháp này, bạn có thể chỉ định nhiều hơn hai tham chiếu trong truy vấn của mình, điều mà bạn không thể làm với cú pháp hai chấm.
Ví dụ, nếu bạn muốn xem tất cả các commit có thể truy cập được từ `refA` hoặc `refB` nhưng không phải từ `refC`, bạn có thể sử dụng một trong hai lệnh sau:
[source,console]
----
$ git log refA refB ^refC
$ git log refA refB --not refC
----
Điều này tạo nên một hệ thống truy vấn phiên bản rất mạnh mẽ sẽ giúp bạn tìm ra những gì có trong các nhánh của mình.
[[_triple_dot]]
===== Ba Chấm (Triple Dot)
Cú pháp chọn khoảng chính cuối cùng là cú pháp ba chấm, chỉ định tất cả các commit có thể truy cập được bởi _một trong hai_ tham chiếu nhưng không phải bởi cả hai.
Nhìn lại lịch sử commit ví dụ trong <<double_dot>>.
Nếu bạn muốn xem những gì có trong `master` hoặc `experiment` nhưng không phải bất kỳ tham chiếu chung nào, bạn có thể chạy:
[source,console]
----
$ git log master...experiment
F
E
D
C
----
Một lần nữa, điều này cung cấp cho bạn đầu ra `log` bình thường nhưng chỉ hiển thị cho bạn thông tin commit cho bốn commit đó, xuất hiện theo thứ tự ngày commit truyền thống.
Một chuyển đổi phổ biến để sử dụng với lệnh `log` trong trường hợp này là `--left-right`, hiển thị cho bạn mỗi commit nằm ở phía nào của khoảng.
Điều này giúp làm cho đầu ra hữu ích hơn:
[source,console]
----
$ git log --left-right master...experiment
< F
< E
> D
> C
----
Với các công cụ này, bạn có thể dễ dàng hơn nhiều để cho Git biết commit hoặc các commit nào bạn muốn kiểm tra.
[[_interactive_staging]]
=== Tổ chức Tương tác
Trong phần này, bạn sẽ xem xét một vài lệnh Git tương tác có thể giúp bạn tạo các cam kết của mình để chỉ bao gồm các sự kết hợp và các phần nhất định của các tệp.
Các công cụ này hữu ích nếu bạn sửa đổi nhiều tệp một cách rộng rãi, sau đó quyết định rằng bạn muốn các thay đổi đó được phân vùng thành một vài cam kết tập trung thay vì một cam kết lớn lộn xộn.
Bằng cách này, bạn có thể đảm bảo các cam kết của bạn là các tập hợp thay đổi được tách biệt một cách hợp lý và có thể được xem xét dễ dàng bởi các nhà phát triển làm việc cùng bạn.
Nếu bạn chạy `git add` với tùy chọn `-i` hoặc `--interactive`, Git sẽ vào chế độ shell tương tác, hiển thị một cái gì đó như thế này:
[source,console]
----
$ git add -i
staged unstaged path
1: unchanged +0/-1 TODO
2: unchanged +1/-1 index.html
3: unchanged +5/-1 lib/simplegit.rb
*** Commands ***
1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked
5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp
What now>
----
Bạn có thể thấy rằng lệnh này cho bạn thấy một cái nhìn rất khác về khu vực tổ chức của bạn so với những gì bạn có lẽ đã quen thuộc -- về cơ bản, thông tin tương tự bạn nhận được với `git status` nhưng ngắn gọn và nhiều thông tin hơn một chút.
Nó liệt kê các thay đổi bạn đã tổ chức ở bên trái và các thay đổi chưa tổ chức ở bên phải.
Sau đó là phần "`Commands`", cho phép bạn thực hiện một số việc như tổ chức và hủy tổ chức tệp, tổ chức các phần của tệp, thêm các tệp chưa được theo dõi và hiển thị các diff của những gì đã được tổ chức.
==== Tổ chức và Hủy tổ chức Tệp
Nếu bạn gõ `u` hoặc `2` (cho cập nhật) tại dấu nhắc `What now>`, bạn sẽ được hỏi tệp nào bạn muốn tổ chức:
[source,console]
----
What now> u
staged unstaged path
1: unchanged +0/-1 TODO
2: unchanged +1/-1 index.html
3: unchanged +5/-1 lib/simplegit.rb
Update>>
----
Để tổ chức các tệp `TODO` và `index.html`, bạn có thể gõ các số:
[source,console]
----
Update>> 1,2
staged unstaged path
* 1: unchanged +0/-1 TODO
* 2: unchanged +1/-1 index.html
3: unchanged +5/-1 lib/simplegit.rb
Update>>
----
Dấu `*` bên cạnh mỗi tệp có nghĩa là tệp đã được chọn để tổ chức.
Nếu bạn nhấn Enter sau khi không gõ gì tại dấu nhắc `Update>>`, Git sẽ lấy bất kỳ thứ gì được chọn và tổ chức nó cho bạn:
[source,console]
----
Update>>
updated 2 paths
*** Commands ***
1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked
5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp
What now> s
staged unstaged path
1: +0/-1 nothing TODO
2: +1/-1 nothing index.html
3: unchanged +5/-1 lib/simplegit.rb
----
Bây giờ bạn có thể thấy rằng các tệp `TODO` và `index.html` đã được tổ chức và tệp `simplegit.rb` vẫn chưa được tổ chức.
Nếu bạn muốn hủy tổ chức tệp `TODO` tại thời điểm này, bạn sử dụng tùy chọn `r` hoặc `3` (cho hoàn nguyên):
[source,console]
----
*** Commands ***
1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked
5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp
What now> r
staged unstaged path
1: +0/-1 nothing TODO
2: +1/-1 nothing index.html
3: unchanged +5/-1 lib/simplegit.rb
Revert>> 1
staged unstaged path
* 1: +0/-1 nothing TODO
2: +1/-1 nothing index.html
3: unchanged +5/-1 lib/simplegit.rb
Revert>> [enter]
reverted one path
----
Nhìn vào trạng thái Git của bạn một lần nữa, bạn có thể thấy rằng bạn đã hủy tổ chức tệp `TODO`:
[source,console]
----
*** Commands ***
1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked
5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp
What now> s
staged unstaged path
1: unchanged +0/-1 TODO
2: +1/-1 nothing index.html
3: unchanged +5/-1 lib/simplegit.rb
----
Để xem diff của những gì bạn đã tổ chức, bạn có thể sử dụng lệnh `d` hoặc `6` (cho diff).
Nó hiển thị cho bạn một danh sách các tệp đã tổ chức của bạn, và bạn có thể chọn những tệp mà bạn muốn xem diff đã tổ chức.
Điều này giống như chỉ định `git diff --cached` trên dòng lệnh:
[source,console]
----
*** Commands ***
1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked
5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp
What now> d
staged unstaged path
1: +1/-1 nothing index.html
Review diff>> 1
diff --git a/index.html b/index.html
index 4d07108..4335f49 100644
--- a/index.html
+++ b/index.html
@@ -16,7 +16,7 @@ Date Finder
<p id="out">...</p>
-<div id="footer">contact : support@github.com</div>
+<div id="footer">contact : email.support@github.com</div>
<script type="text/javascript">
----
Với các lệnh cơ bản này, bạn có thể sử dụng chế độ thêm tương tác để xử lý khu vực tổ chức của mình dễ dàng hơn một chút.
==== Tổ chức các Bản vá
Cũng có thể để Git tổ chức các _phần_ nhất định của các tệp chứ không phải phần còn lại.
Ví dụ, nếu bạn thực hiện hai thay đổi cho tệp `simplegit.rb` của mình và muốn tổ chức một trong số chúng chứ không phải cái kia, việc làm điều đó rất dễ dàng trong Git.
Từ dấu nhắc tương tác tương tự được giải thích trong phần trước, gõ `p` hoặc `5` (cho bản vá).
Git sẽ hỏi bạn tệp nào bạn muốn tổ chức một phần; sau đó, đối với mỗi phần của các tệp đã chọn, nó sẽ hiển thị các đoạn diff của tệp và hỏi bạn có muốn tổ chức chúng hay không, từng cái một:
[source,console]
----
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index dd5ecc4..57399e0 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -22,7 +22,7 @@ class SimpleGit
end
def log(treeish = 'master')
- command("git log -n 25 #{treeish}")
+ command("git log -n 30 #{treeish}")
end
def blame(path)
Stage this hunk [y,n,a,d,/,j,J,g,e,?]?
----
Bạn có rất nhiều lựa chọn tại thời điểm này.
Gõ `?` hiển thị danh sách những gì bạn có thể làm:
[source,console]
----
Stage this hunk [y,n,a,d,/,j,J,g,e,?]? ?
y - stage this hunk
n - do not stage this hunk
a - stage this and all the remaining hunks in the file
d - do not stage this hunk nor any of the remaining hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help
----
Nói chung, bạn sẽ gõ `y` hoặc `n` nếu bạn muốn tổ chức từng đoạn, nhưng tổ chức tất cả chúng trong một số tệp nhất định hoặc bỏ qua quyết định đoạn cho đến sau này cũng có thể hữu ích.
Nếu bạn tổ chức một phần của tệp và để phần khác chưa được tổ chức, đầu ra trạng thái của bạn sẽ trông như thế này:
[source,console]
----
What now> 1
staged unstaged path
1: unchanged +0/-1 TODO
2: +1/-1 nothing index.html
3: +1/-1 +4/-0 lib/simplegit.rb
----
Trạng thái của tệp `simplegit.rb` rất thú vị.
Nó cho bạn thấy rằng một vài dòng đã được tổ chức và một vài dòng chưa được tổ chức.
Bạn đã tổ chức một phần của tệp này.
Tại thời điểm này, bạn có thể thoát khỏi tập lệnh thêm tương tác và chạy `git commit` để cam kết các tệp đã được tổ chức một phần.
Bạn cũng không cần phải ở chế độ thêm tương tác để thực hiện việc tổ chức tệp một phần -- bạn có thể bắt đầu tập lệnh tương tự bằng cách sử dụng `git add -p` hoặc `git add --patch` trên dòng lệnh.
Hơn nữa, bạn có thể sử dụng chế độ bản vá để đặt lại một phần tệp bằng lệnh `git reset --patch`, để kiểm tra các phần của tệp bằng lệnh `git checkout --patch` và để cất giữ các phần của tệp bằng lệnh `git stash save --patch`.
Chúng ta sẽ đi sâu vào chi tiết hơn về từng cái này khi chúng ta sử dụng các lệnh này nâng cao hơn.
[[_git_stashing]]
=== Cất giấu và Làm sạch (Stashing and Cleaning)
Thường thì, khi bạn đang làm việc trên một phần của dự án, mọi thứ đang ở trong trạng thái lộn xộn và bạn muốn chuyển sang các nhánh khác một chút để làm việc khác.
Vấn đề là, bạn không muốn thực hiện một commit cho công việc còn dang dở chỉ để bạn có thể quay lại điểm này sau.
Câu trả lời cho vấn đề này là lệnh `git stash`.
Cất giấu (Stashing) lấy trạng thái bẩn của thư mục làm việc của bạn -- nghĩa là, các tệp được theo dõi đã sửa đổi và các thay đổi đã tổ chức của bạn -- và lưu nó vào một ngăn xếp các thay đổi chưa hoàn thành mà bạn có thể áp dụng lại bất cứ lúc nào (ngay cả trên một nhánh khác).
[NOTE]
.Di chuyển sang `git stash push`
====
Kể từ cuối tháng 10 năm 2017, đã có cuộc thảo luận rộng rãi trên danh sách gửi thư Git, trong đó lệnh `git stash save` đang bị phản đối để ủng hộ giải pháp thay thế hiện có `git stash push`.
Lý do chính cho điều này là `git stash push` giới thiệu tùy chọn cất giấu các _đặc tả đường dẫn_ (pathspecs) đã chọn, điều mà `git stash save` không hỗ trợ.
`git stash save` sẽ không biến mất sớm, vì vậy đừng lo lắng về việc nó đột ngột biến mất.
Nhưng bạn có thể muốn bắt đầu chuyển sang giải pháp thay thế `push` cho chức năng mới.
====
==== Cất giấu Công việc của Bạn
Để minh họa việc cất giấu, bạn sẽ vào dự án của mình và bắt đầu làm việc trên một vài tệp và có thể tổ chức một trong những thay đổi.
Nếu bạn chạy `git status`, bạn có thể thấy trạng thái bẩn của mình:
[source,console]
----
$ git status
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib/simplegit.rb
----
Bây giờ bạn muốn chuyển nhánh, nhưng bạn chưa muốn commit những gì bạn đang làm việc, vì vậy bạn sẽ cất giấu các thay đổi.
Để đẩy một stash mới vào ngăn xếp của bạn, hãy chạy `git stash` hoặc `git stash push`:
[source,console]
----
$ git stash
Saved working directory and index state \
"WIP on master: 049d078 Create index file"
HEAD is now at 049d078 Create index file
(To restore them type "git stash apply")
----
Bây giờ bạn có thể thấy rằng thư mục làm việc của bạn đã sạch:
[source,console]
----
$ git status
# On branch master
nothing to commit, working directory clean
----
Tại thời điểm này, bạn có thể chuyển nhánh và làm việc ở nơi khác; các thay đổi của bạn được lưu trữ trên ngăn xếp của bạn.
Để xem những stash nào bạn đã lưu trữ, bạn có thể sử dụng `git stash list`:
[source,console]
----
$ git stash list
stash@{0}: WIP on master: 049d078 Create index file
stash@{1}: WIP on master: c264051 Revert "Add file_size"
stash@{2}: WIP on master: 21d80a5 Add number to log
----
Trong trường hợp này, hai stash đã được lưu trước đó, vì vậy bạn có quyền truy cập vào ba công việc được cất giấu khác nhau.
Bạn có thể áp dụng lại cái bạn vừa cất giấu bằng cách sử dụng lệnh được hiển thị trong đầu ra trợ giúp của lệnh stash ban đầu: `git stash apply`.
Nếu bạn muốn áp dụng một trong những stash cũ hơn, bạn có thể chỉ định nó bằng cách đặt tên cho nó, như thế này: `git stash apply stash@{2}`.
Nếu bạn không chỉ định một stash, Git giả định stash gần đây nhất và cố gắng áp dụng nó:
[source,console]
----
$ git stash apply
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: index.html
modified: lib/simplegit.rb
no changes added to commit (use "git add" and/or "git commit -a")
----
Bạn có thể thấy rằng Git sửa đổi lại các tệp bạn đã hoàn nguyên khi bạn lưu stash.
Trong trường hợp này, bạn đã có một thư mục làm việc sạch khi bạn cố gắng áp dụng stash, và bạn đã cố gắng áp dụng nó trên cùng một nhánh bạn đã lưu nó.
Việc có một thư mục làm việc sạch và áp dụng nó trên cùng một nhánh là không cần thiết để áp dụng thành công một stash.
Bạn có thể lưu một stash trên một nhánh, chuyển sang một nhánh khác sau đó, và cố gắng áp dụng lại các thay đổi.
Bạn cũng có thể có các tệp đã sửa đổi và chưa commit trong thư mục làm việc của mình khi bạn áp dụng một stash -- Git cung cấp cho bạn các xung đột hợp nhất nếu bất cứ điều gì không còn áp dụng sạch sẽ nữa.
Các thay đổi đối với các tệp của bạn đã được áp dụng lại, nhưng tệp bạn đã tổ chức trước đó không được tổ chức lại.
Để làm điều đó, bạn phải chạy lệnh `git stash apply` với tùy chọn `--index` để bảo lệnh cố gắng áp dụng lại các thay đổi đã tổ chức.
Nếu bạn đã chạy lệnh đó thay thế, bạn sẽ quay lại vị trí ban đầu của mình:
[source,console]
----
$ git stash apply --index
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib/simplegit.rb
----
Tùy chọn `apply` chỉ cố gắng áp dụng công việc đã cất giấu -- bạn vẫn giữ nó trên ngăn xếp của mình.
Để xóa nó, bạn có thể chạy `git stash drop` với tên của stash cần xóa:
[source,console]
----
$ git stash list
stash@{0}: WIP on master: 049d078 Create index file
stash@{1}: WIP on master: c264051 Revert "Add file_size"
stash@{2}: WIP on master: 21d80a5 Add number to log
$ git stash drop stash@{0}
Dropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43)
----
Bạn cũng có thể chạy `git stash pop` để áp dụng stash và sau đó ngay lập tức xóa nó khỏi ngăn xếp của bạn.
==== Cất giấu Sáng tạo
Có một vài biến thể stash cũng có thể hữu ích.
Tùy chọn đầu tiên khá phổ biến là tùy chọn `--keep-index` cho lệnh `git stash`.
Điều này bảo Git không chỉ bao gồm tất cả nội dung đã tổ chức trong stash đang được tạo, mà đồng thời để lại nó trong chỉ mục (index).
[source,console]
----
$ git status -s
M index.html
M lib/simplegit.rb
$ git stash --keep-index
Saved working directory and index state WIP on master: 1b65b17 added the index file
HEAD is now at 1b65b17 added the index file
$ git status -s
M index.html
----
Một điều phổ biến khác bạn có thể muốn làm với stash là cất giấu các tệp không được theo dõi cũng như các tệp được theo dõi.
Theo mặc định, `git stash` sẽ chỉ cất giấu các tệp _được theo dõi_ đã sửa đổi và đã tổ chức.
Nếu bạn chỉ định `--include-untracked` hoặc `-u`, Git sẽ bao gồm các tệp không được theo dõi trong stash đang được tạo.
Tuy nhiên, việc bao gồm các tệp không được theo dõi trong stash vẫn sẽ không bao gồm các tệp bị _bỏ qua_ một cách rõ ràng; để bao gồm thêm các tệp bị bỏ qua, hãy sử dụng `--all` (hoặc chỉ `-a`).
[source,console]
----
$ git status -s
M index.html
M lib/simplegit.rb
?? new-file.txt
$ git stash -u
Saved working directory and index state WIP on master: 1b65b17 added the index file
HEAD is now at 1b65b17 added the index file
$ git status -s
$
----
Cuối cùng, nếu bạn chỉ định cờ `--patch`, Git sẽ không cất giấu mọi thứ bị sửa đổi mà thay vào đó sẽ nhắc bạn một cách tương tác xem thay đổi nào bạn muốn cất giấu và thay đổi nào bạn muốn giữ trong thư mục làm việc của mình.
[source,console]
----
$ git stash --patch
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index 66d332e..8bb5674 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -16,6 +16,10 @@ class SimpleGit
return `#{git_cmd} 2>&1`.chomp
end
end
+
+ def show(treeish = 'master')
+ command("git show #{treeish}")
+ end
end
test
Stash this hunk [y,n,q,a,d,/,e,?]? y
Saved working directory and index state WIP on master: 1b65b17 added the index file
----
==== Tạo một Nhánh từ một Stash
Nếu bạn cất giấu một số công việc, để nó ở đó một thời gian, và tiếp tục trên nhánh mà bạn đã cất giấu công việc, bạn có thể gặp vấn đề khi áp dụng lại công việc.
Nếu việc áp dụng cố gắng sửa đổi một tệp mà bạn đã sửa đổi kể từ đó, bạn sẽ nhận được xung đột hợp nhất và sẽ phải cố gắng giải quyết nó.
Nếu bạn muốn một cách dễ dàng hơn để kiểm tra lại các thay đổi đã cất giấu, bạn có thể chạy `git stash branch <tên nhánh mới>`, lệnh này tạo một nhánh mới cho bạn với tên nhánh bạn đã chọn, checkout commit bạn đang ở khi bạn cất giấu công việc của mình, áp dụng lại công việc của bạn ở đó, và sau đó xóa stash nếu nó áp dụng thành công:
[source,console]
----
$ git stash branch testchanges
M index.html
M lib/simplegit.rb
Switched to a new branch 'testchanges'
On branch testchanges
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib/simplegit.rb
Dropped refs/stash@{0} (29d385a81d163dfd45a452a2ce816487a6b8b014)
----
Đây là một phím tắt hay để khôi phục công việc đã cất giấu một cách dễ dàng và làm việc trên nó trong một nhánh mới.
[[_git_clean]]
==== Làm sạch Thư mục Làm việc của bạn
Cuối cùng, bạn có thể không muốn cất giấu một số công việc hoặc tệp trong thư mục làm việc của mình, mà chỉ đơn giản là loại bỏ chúng; đó là mục đích của lệnh `git clean`.
Một số lý do phổ biến để làm sạch thư mục làm việc của bạn có thể là để loại bỏ rác đã được tạo ra bởi các lần hợp nhất hoặc các công cụ bên ngoài hoặc để loại bỏ các tạo phẩm xây dựng (build artifacts) để chạy một bản build sạch.
Bạn sẽ muốn khá cẩn thận với lệnh này, vì nó được thiết kế để xóa các tệp khỏi thư mục làm việc của bạn mà không được theo dõi.
Nếu bạn thay đổi ý định, thường không có cách nào lấy lại nội dung của các tệp đó.
Một tùy chọn an toàn hơn là chạy `git stash --all` để xóa mọi thứ nhưng lưu nó trong một stash.
Giả sử bạn thực sự muốn xóa các tệp rác hoặc làm sạch thư mục làm việc của mình, bạn có thể làm như vậy với `git clean`.
Để xóa tất cả các tệp không được theo dõi trong thư mục làm việc của bạn, bạn có thể chạy `git clean -f -d`, lệnh này xóa bất kỳ tệp nào và cũng xóa bất kỳ thư mục con nào trở nên trống rỗng do kết quả đó.
`-f` có nghĩa là 'force' (buộc) hoặc "`thực sự làm điều này,`" và là bắt buộc nếu biến cấu hình Git `clean.requireForce` không được đặt rõ ràng là false.
Nếu bạn muốn xem nó sẽ làm gì, bạn có thể chạy lệnh với tùy chọn `--dry-run` (hoặc `-n`), có nghĩa là "`chạy thử và cho tôi biết bạn _sẽ_ xóa những gì`".
[source,console]
----
$ git clean -d -n
Would remove test.o
Would remove tmp/
----
Theo mặc định, lệnh `git clean` sẽ chỉ xóa các tệp không được theo dõi mà không bị bỏ qua.
Bất kỳ tệp nào khớp với một mẫu trong `.gitignore` hoặc các tệp bỏ qua khác của bạn sẽ không bị xóa.
Nếu bạn muốn xóa cả những tệp đó, chẳng hạn như để xóa tất cả các tệp `.o` được tạo ra từ một bản build để bạn có thể thực hiện một bản build hoàn toàn sạch, bạn có thể thêm `-x` vào lệnh `clean`.
[source,console]
----
$ git status -s
M lib/simplegit.rb
?? build.TMP
?? tmp/
$ git clean -n -d
Would remove build.TMP
Would remove tmp/
$ git clean -n -d -x
Would remove build.TMP
Would remove test.o
Would remove tmp/
----
Nếu bạn không biết lệnh `git clean` sẽ làm gì, hãy luôn chạy nó với `-n` trước để kiểm tra kỹ trước khi thay đổi `-n` thành `-f` và thực hiện nó thực sự.
Cách khác bạn có thể cẩn thận về quy trình là chạy nó với cờ `-i` hoặc "`interactive`" (tương tác).
Điều này sẽ chạy lệnh `clean` trong chế độ tương tác.
[source,console]
----
$ git clean -x -i
Would remove the following items:
build.TMP test.o
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers 4: ask each 5: quit
6: help
What now>
----
Bằng cách này, bạn có thể bước qua từng tệp riêng lẻ hoặc chỉ định các mẫu để xóa một cách tương tác.
[NOTE]
====
Có một tình huống kỳ quặc mà bạn có thể cần phải cực kỳ mạnh mẽ trong việc yêu cầu Git làm sạch thư mục làm việc của bạn.
Nếu bạn tình cờ ở trong một thư mục làm việc mà dưới đó bạn đã sao chép hoặc clone các kho lưu trữ Git khác (có lẽ là dưới dạng các mô-đun con), ngay cả `git clean -fd` cũng sẽ từ chối xóa các thư mục đó.
Trong những trường hợp như vậy, bạn cần thêm tùy chọn `-f` thứ hai để nhấn mạnh.
====
[[_signing]]
=== Ký Công việc của Bạn (Signing Your Work)
Git an toàn về mặt mật mã, nhưng nó không phải là tuyệt đối an toàn.
Nếu bạn đang lấy công việc từ những người khác trên internet và muốn xác minh rằng các commit thực sự đến từ một nguồn đáng tin cậy, Git có một vài cách để ký và xác minh công việc bằng cách sử dụng GPG.
==== Giới thiệu về GPG
Trước hết, nếu bạn muốn ký bất cứ thứ gì, bạn cần phải cấu hình GPG và cài đặt khóa cá nhân của mình.
[source,console]
----
$ gpg --list-keys
/Users/schacon/.gnupg/pubring.gpg
pub 2048R/0A46826A 2014-06-04 uid Scott Chacon (Git signing key) <schacon@gmail.com> sub 2048R/874529A9 2014-06-04
Nếu bạn chưa cài đặt khóa, bạn có thể tạo một khóa bằng lệnh `gpg --gen-key`. [source,console]
$ gpg --gen-key
Khi bạn đã có một khóa cá nhân để ký, bạn có thể cấu hình Git để sử dụng nó cho việc ký mọi thứ bằng cách thiết lập cài đặt cấu hình `user.signingkey`. [source,console]
$ git config --global user.signingkey 0A46826A!
Bây giờ Git sẽ sử dụng khóa của bạn theo mặc định để ký các thẻ (tag) và commit nếu bạn muốn. ==== Ký Thẻ (Signing Tags) Nếu bạn đã thiết lập khóa cá nhân GPG, bây giờ bạn có thể sử dụng nó để ký các thẻ mới. Tất cả những gì bạn phải làm là sử dụng `-s` thay vì `-a`: [source,console]
$ git tag -s v1.5 -m 'my signed 1.5 tag'
You need a passphrase to unlock the secret key for user: "Ben Straub <ben@straub.cc>" 2048-bit RSA key, ID 800430EB, created 2014-05-04
Nếu bạn chạy `git show` trên thẻ đó, bạn có thể thấy chữ ký GPG của mình được đính kèm với nó: [source,console]
$ git show v1.5 tag v1.5 Tagger: Ben Straub <ben@straub.cc> Date: Sat May 3 20:29:41 2014 -0700
my signed 1.5 tag -----BEGIN PGP SIGNATURE----- Version: GnuPG v1
iQEcBAABAgAGBQJTZbQlAAoJEF0+sviABDDrZbQH/09PfE51KPVPlanr6q1v4/Ut LQxfojUWiLQdg2ESJItkcuweYg+kc3HCyFejeDIBw9dpXt00rY26p05qrpnG+85b hM1/PswpPLuBSr+oCIDj5GMC2r2iEKsfv2fJbNW8iWAXVLoWZRF8B0MfqX/YTMbm ecorc4iXzQu7tupRihslbNkfvfciMnSDeSvzCpWAHl7h8Wj6hhqePmLm9lAYqnKp 8S5B/1SSQuEAjRZgI4IexpZoeKGVDptPHxLLS38fozsyi0QyDyzEgJxcJQVMXxVi RUysgqjcpT8+iQM1PblGfHR4XAhuOqN5Fx06PSaFZhqvWFezJ28/CLyX5q+oIVk= =EFTF -----END PGP SIGNATURE-----
commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <schacon@gee-mail.com> Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
==== Xác minh Thẻ (Verifying Tags) Để xác minh một thẻ đã ký, bạn sử dụng `git tag -v <tên-thẻ>`. Lệnh này sử dụng GPG để xác minh chữ ký. Bạn cần khóa công khai của người ký trong chùm chìa khóa (keyring) của mình để điều này hoạt động bình thường: [source,console]
$ git tag -v v1.4.2.1 object 883653babd8ee7ea23e6a5c392bb739348b1eb61 type commit tag v1.4.2.1 tagger Junio C Hamano <junkio@cox.net> 1158138501 -0700
GIT 1.4.2.1
Minor fixes since 1.4.2, including git-mv and git-http with alternates. gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A gpg: Good signature from "Junio C Hamano <junkio@cox.net>" gpg: aka "[jpeg image of size 1513]" Primary key fingerprint: 3565 2A26 2040 E066 C9A7 4A7D C0C6 D9A4 F311 9B9A
Nếu bạn không có khóa công khai của người ký, bạn sẽ nhận được một cái gì đó giống như thế này thay thế: [source,console]
gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A gpg: Can’t check signature: public key not found error: could not verify the tag 'v1.4.2.1'
[[_signing_commits]] ==== Ký Commit (Signing Commits) Trong các phiên bản gần đây hơn của Git (v1.7.9 trở lên), bây giờ bạn cũng có thể ký các commit riêng lẻ. Nếu bạn quan tâm đến việc ký các commit trực tiếp thay vì chỉ các thẻ, tất cả những gì bạn cần làm là thêm `-S` vào lệnh `git commit` của bạn. [source,console]
$ git commit -a -S -m 'Signed commit'
You need a passphrase to unlock the secret key for user: "Scott Chacon (Git signing key) <schacon@gmail.com>" 2048-bit RSA key, ID 0A46826A, created 2014-06-04
[master 5c3386c] Signed commit 4 files changed, 4 insertions(+), 24 deletions(-) rewrite Rakefile (100%) create mode 100644 lib/git.rb
Để xem và xác minh các chữ ký này, cũng có một tùy chọn `--show-signature` cho `git log`. [source,console]
$ git log --show-signature -1 commit 5c3386cf54bba0a33a32da706aa52bc0155503c2 gpg: Signature made Wed Jun 4 19:49:17 2014 PDT using RSA key ID 0A46826A gpg: Good signature from "Scott Chacon (Git signing key) <schacon@gmail.com>" Author: Scott Chacon <schacon@gmail.com> Date: Wed Jun 4 19:49:17 2014 -0700
Signed commit
Ngoài ra, bạn có thể cấu hình `git log` để kiểm tra bất kỳ chữ ký nào nó tìm thấy và liệt kê chúng trong đầu ra của nó với định dạng `%G?`. [source,console]
$ git log --pretty="format:%h %G? %aN %s"
5c3386c G Scott Chacon Signed commit ca82a6d N Scott Chacon Change the version number 085bb3b N Scott Chacon Remove unnecessary test code a11bef0 N Scott Chacon Initial commit
Ở đây chúng ta có thể thấy rằng chỉ có commit mới nhất được ký và hợp lệ, còn các commit trước đó thì không. Trong Git 1.8.3 và mới hơn, `git merge` và `git pull` có thể được bảo để kiểm tra và từ chối khi hợp nhất một commit không mang chữ ký GPG đáng tin cậy với lệnh `--verify-signatures`. Nếu bạn sử dụng tùy chọn này khi hợp nhất một nhánh và nó chứa các commit không được ký và hợp lệ, việc hợp nhất sẽ không hoạt động. [source,console]
$ git merge --verify-signatures non-verify fatal: Commit ab06180 does not have a GPG signature.
Nếu việc hợp nhất chỉ chứa các commit đã ký hợp lệ, lệnh hợp nhất sẽ hiển thị cho bạn tất cả các chữ ký mà nó đã kiểm tra và sau đó tiến hành hợp nhất. [source,console]
$ git merge --verify-signatures signed-branch Commit 13ad65e has a good GPG signature by Scott Chacon (Git signing key) <schacon@gmail.com> Updating 5c3386c..13ad65e Fast-forward README | 2 + 1 file changed, 2 insertions()
Bạn cũng có thể sử dụng tùy chọn `-S` với lệnh `git merge` để ký chính commit hợp nhất kết quả. Ví dụ sau đây vừa xác minh rằng mọi commit trong nhánh được hợp nhất đều được ký và hơn nữa ký commit hợp nhất kết quả. [source,console]
$ git merge --verify-signatures -S signed-branch Commit 13ad65e has a good GPG signature by Scott Chacon (Git signing key) <schacon@gmail.com>
You need a passphrase to unlock the secret key for user: "Scott Chacon (Git signing key) <schacon@gmail.com>" 2048-bit RSA key, ID 0A46826A, created 2014-06-04
Merge made by the 'recursive' strategy. README | 2 + 1 file changed, 2 insertions()
==== Mọi người Phải Ký Việc ký các thẻ và commit là rất tuyệt, nhưng nếu bạn quyết định sử dụng điều này trong quy trình làm việc bình thường của mình, bạn sẽ phải đảm bảo rằng mọi người trong nhóm của bạn hiểu cách thực hiện điều đó. Điều này có thể đạt được bằng cách yêu cầu mọi người làm việc với kho lưu trữ chạy `git config --local commit.gpgsign true` để tự động ký tất cả các commit của họ trong kho lưu trữ theo mặc định. Nếu bạn không làm vậy, bạn sẽ dành rất nhiều thời gian để giúp mọi người tìm ra cách viết lại các commit của họ với các phiên bản đã ký. Hãy chắc chắn rằng bạn hiểu GPG và lợi ích của việc ký mọi thứ trước khi áp dụng điều này như một phần của quy trình làm việc tiêu chuẩn của bạn. [[_searching]] === Tìm kiếm (Searching) Với bất kỳ cơ sở mã nào có kích thước bất kỳ, bạn sẽ thường cần tìm nơi một hàm được gọi hoặc định nghĩa, hoặc hiển thị lịch sử của một phương thức. Git cung cấp một vài công cụ hữu ích để tìm kiếm qua mã và các commit được lưu trữ trong cơ sở dữ liệu của nó một cách nhanh chóng và dễ dàng. Chúng ta sẽ đi qua một vài trong số chúng. [[_git_grep]] ==== Git Grep Git đi kèm với một lệnh gọi là `grep` cho phép bạn dễ dàng tìm kiếm qua bất kỳ cây (tree) đã commit nào, thư mục làm việc, hoặc thậm chí là chỉ mục (index) cho một chuỗi hoặc biểu thức chính quy. Đối với các ví dụ sau, chúng ta sẽ tìm kiếm qua mã nguồn của chính Git. Theo mặc định, `git grep` sẽ tìm qua các tệp trong thư mục làm việc của bạn. Là một biến thể đầu tiên, bạn có thể sử dụng một trong các tùy chọn `-n` hoặc `--line-number` để in ra các số dòng nơi Git đã tìm thấy các kết quả khớp: [source,console]
$ git grep -n gmtime_r compat/gmtime.c:3:#undef gmtime_r compat/gmtime.c:8: return git_gmtime_r(timep, &result); compat/gmtime.c:11:struct tm git_gmtime_r(const time_t *timep, struct tm *result) compat/gmtime.c:16: ret = gmtime_r(timep, result); compat/mingw.c:826:struct tm *gmtime_r(const time_t *timep, struct tm *result) compat/mingw.h:206:struct tm *gmtime_r(const time_t *timep, struct tm *result); date.c:482: if (gmtime_r(&now, &now_tm)) date.c:545: if (gmtime_r(&time, tm)) { date.c:758: / gmtime_r() in match_digit() may have clobbered it */ git-compat-util.h:1138:struct tm *git_gmtime_r(const time_t *, struct tm *); git-compat-util.h:1140:#define gmtime_r git_gmtime_r
Ngoài tìm kiếm cơ bản được hiển thị ở trên, `git grep` hỗ trợ vô số tùy chọn thú vị khác. Ví dụ, thay vì in tất cả các kết quả khớp, bạn có thể yêu cầu `git grep` tóm tắt đầu ra bằng cách chỉ hiển thị cho bạn những tệp nào chứa chuỗi tìm kiếm và có bao nhiêu kết quả khớp trong mỗi tệp với tùy chọn `-c` hoặc `--count`: [source,console]
$ git grep --count gmtime_r compat/gmtime.c:4 compat/mingw.c:1 compat/mingw.h:1 date.c:3 git-compat-util.h:2
Nếu bạn quan tâm đến _ngữ cảnh_ của một chuỗi tìm kiếm, bạn có thể hiển thị phương thức hoặc hàm bao quanh cho mỗi chuỗi khớp với một trong các tùy chọn `-p` hoặc `--show-function`: [source,console]
$ git grep -p gmtime_r .c date.c=static int match_multi_number(timestamp_t num, char c, const char *date, date.c: if (gmtime_r(&now, &now_tm)) date.c=static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt) date.c: if (gmtime_r(&time, tm)) { date.c=int parse_date_basic(const char *date, timestamp_t *timestamp, int *offset) date.c: / gmtime_r() in match_digit() may have clobbered it */
Như bạn có thể thấy, thủ tục `gmtime_r` được gọi từ cả hai hàm `match_multi_number` và `match_digit` trong tệp `date.c` (kết quả khớp thứ ba được hiển thị đại diện cho chỉ chuỗi xuất hiện trong một bình luận). Bạn cũng có thể tìm kiếm các kết hợp phức tạp của các chuỗi với cờ `--and`, đảm bảo rằng nhiều kết quả khớp phải xảy ra trong cùng một dòng văn bản. Ví dụ, hãy tìm bất kỳ dòng nào định nghĩa một hằng số có tên chứa _một trong hai_ chuỗi con "`LINK`" hoặc "`BUF_MAX`", cụ thể trong một phiên bản cũ hơn của cơ sở mã Git được đại diện bởi thẻ `v1.8.0` (chúng ta sẽ thêm các tùy chọn `--break` và `--heading` giúp chia nhỏ đầu ra thành định dạng dễ đọc hơn): [source,console]
$ git grep --break --heading \ -n -e '#define' --and \( -e LINK -e BUF_MAX \) v1.8.0 v1.8.0:builtin/index-pack.c 62:#define FLAG_LINK (1u<<20)
v1.8.0:cache.h 73:#define S_IFGITLINK 0160000 74:#define S_ISGITLINK(m) (((m) & S_IFMT) == S_IFGITLINK)
v1.8.0:environment.c 54:#define OBJECT_CREATION_MODE OBJECT_CREATION_USES_HARDLINKS
v1.8.0:strbuf.c 326:#define STRBUF_MAXLINK (2*PATH_MAX)
v1.8.0:symlinks.c 53:#define FL_SYMLINK (1 << 2)
v1.8.0:zlib.c 30:/* #define ZLIB_BUF_MAX ((uInt)-1) / 31:#define ZLIB_BUF_MAX ((uInt) 1024 * 1024 * 1024) / 1GB */
Lệnh `git grep` có một vài lợi thế so với các lệnh tìm kiếm thông thường như `grep` và `ack`. Đầu tiên là nó thực sự nhanh, thứ hai là bạn có thể tìm kiếm qua bất kỳ cây nào trong Git, không chỉ thư mục làm việc. Như chúng ta đã thấy trong ví dụ trên, chúng ta đã tìm kiếm các thuật ngữ trong một phiên bản cũ hơn của mã nguồn Git, không phải phiên bản hiện đang được checkout. ==== Tìm kiếm Nhật ký Git (Git Log Searching) Có lẽ bạn không tìm kiếm _nơi_ một thuật ngữ tồn tại, mà là _khi nào_ nó tồn tại hoặc được giới thiệu. Lệnh `git log` có một số công cụ mạnh mẽ để tìm các commit cụ thể bằng nội dung thông điệp của chúng hoặc thậm chí nội dung của diff mà chúng giới thiệu. Ví dụ, nếu chúng ta muốn tìm ra khi nào hằng số `ZLIB_BUF_MAX` được giới thiệu ban đầu, chúng ta có thể sử dụng tùy chọn `-S` (thường được gọi là tùy chọn Git "`pickaxe`" (cái cuốc)) để bảo Git chỉ hiển thị cho chúng ta những commit đã thay đổi số lần xuất hiện của chuỗi đó. [source,console]
$ git log -S ZLIB_BUF_MAX --oneline e01503b zlib: allow feeding more than 4GB in one go ef49a7a zlib: zlib can only process 4GB at a time
Nếu chúng ta nhìn vào diff của các commit đó, chúng ta có thể thấy rằng trong `ef49a7a` hằng số đã được giới thiệu và trong `e01503b` nó đã được sửa đổi. Nếu bạn cần cụ thể hơn, bạn có thể cung cấp một biểu thức chính quy để tìm kiếm với tùy chọn `-G`. ===== Tìm kiếm Nhật ký Dòng (Line Log Search) Một tìm kiếm nhật ký khá nâng cao khác cực kỳ hữu ích là tìm kiếm lịch sử dòng. Chỉ cần chạy `git log` với tùy chọn `-L`, và nó sẽ hiển thị cho bạn lịch sử của một hàm hoặc dòng mã trong cơ sở mã của bạn. Ví dụ, nếu chúng ta muốn xem mọi thay đổi được thực hiện đối với hàm `git_deflate_bound` trong tệp `zlib.c`, chúng ta có thể chạy `git log -L :git_deflate_bound:zlib.c`. Điều này sẽ cố gắng tìm ra giới hạn của hàm đó là gì và sau đó nhìn qua lịch sử và hiển thị cho chúng ta mọi thay đổi đã được thực hiện đối với hàm như một loạt các bản vá quay trở lại khi hàm được tạo lần đầu tiên. [source,console]
$ git log -L :git_deflate_bound:zlib.c commit ef49a7a0126d64359c974b4b3b71d7ad42ee3bca Author: Junio C Hamano <gitster@pobox.com> Date: Fri Jun 10 11:52:15 2011 -0700
zlib: zlib can only process 4GB at a time
diff --git a/zlib.c b/zlib.c --- a/zlib.c + b/zlib.c @@ -85,5 +130,5 @@ -unsigned long git_deflate_bound(z_streamp strm, unsigned long size) +unsigned long git_deflate_bound(git_zstream *strm, unsigned long size) { - return deflateBound(strm, size); + return deflateBound(&strm→z, size); }
commit 225a6f1068f71723a910e8565db4e252b3ca21fa Author: Junio C Hamano <gitster@pobox.com> Date: Fri Jun 10 11:18:17 2011 -0700
zlib: wrap deflateBound() too
diff --git a/zlib.c b/zlib.c --- a/zlib.c + b/zlib.c @@ -81,0 +85,5 @@ +unsigned long git_deflate_bound(z_streamp strm, unsigned long size) +{ + return deflateBound(strm, size); +}
+
Nếu Git không thể tìm ra cách khớp một hàm hoặc phương thức trong ngôn ngữ lập trình của bạn, bạn cũng có thể cung cấp cho nó một biểu thức chính quy (hoặc _regex_). Ví dụ, điều này sẽ thực hiện điều tương tự như ví dụ trên: `git log -L '/unsigned long git_deflate_bound/',/^}/:zlib.c`. Bạn cũng có thể cung cấp cho nó một phạm vi các dòng hoặc một số dòng đơn lẻ và bạn sẽ nhận được cùng một loại đầu ra. [[_rewriting_history]] === Viết lại Lịch sử (Rewriting History) Nhiều lần, khi làm việc với Git, bạn có thể muốn sửa lại lịch sử commit cục bộ của mình. Một trong những điều tuyệt vời về Git là nó cho phép bạn đưa ra quyết định vào giây phút cuối cùng có thể. Bạn có thể quyết định tệp nào sẽ đi vào commit nào ngay trước khi bạn commit với khu vực tổ chức, bạn có thể quyết định rằng bạn chưa có ý định làm việc trên một cái gì đó với `git stash`, và bạn có thể viết lại các commit đã xảy ra để chúng trông giống như chúng đã xảy ra theo một cách khác. Điều này có thể liên quan đến việc thay đổi thứ tự của các commit, thay đổi thông điệp hoặc sửa đổi các tệp trong một commit, gộp (squash) lại với nhau hoặc tách rời các commit, hoặc xóa hoàn toàn các commit -- tất cả trước khi bạn chia sẻ công việc của mình với người khác. Trong phần này, bạn sẽ thấy cách hoàn thành các tác vụ này để bạn có thể làm cho lịch sử commit của mình trông theo cách bạn muốn trước khi bạn chia sẻ nó với người khác. [NOTE] .Đừng đẩy công việc của bạn cho đến khi bạn hài lòng với nó ==== Một trong những quy tắc cốt yếu của Git là, vì rất nhiều công việc là cục bộ trong bản sao của bạn, bạn có rất nhiều tự do để viết lại lịch sử của mình _cục bộ_. Tuy nhiên, một khi bạn đẩy công việc của mình, đó là một câu chuyện hoàn toàn khác, và bạn nên coi công việc đã đẩy là cuối cùng trừ khi bạn có lý do chính đáng để thay đổi nó. Tóm lại, bạn nên tránh đẩy công việc của mình cho đến khi bạn hài lòng với nó và sẵn sàng chia sẻ nó với phần còn lại của thế giới. ==== [[_git_amend]] ==== Thay đổi Commit Cuối cùng Thay đổi commit gần đây nhất của bạn có lẽ là việc viết lại lịch sử phổ biến nhất mà bạn sẽ làm. Bạn sẽ thường muốn làm hai việc cơ bản đối với commit cuối cùng của mình: đơn giản là thay đổi thông điệp commit, hoặc thay đổi nội dung thực tế của commit bằng cách thêm, xóa và sửa đổi các tệp. Nếu bạn chỉ muốn sửa đổi thông điệp commit cuối cùng của mình, điều đó thật dễ dàng: [source,console]
$ git commit --amend
Lệnh trên tải thông điệp commit trước đó vào một phiên soạn thảo, nơi bạn có thể thực hiện các thay đổi đối với thông điệp, lưu các thay đổi đó và thoát. Khi bạn lưu và đóng trình soạn thảo, trình soạn thảo sẽ viết một commit mới chứa thông điệp commit đã cập nhật đó và biến nó thành commit cuối cùng mới của bạn. Mặt khác, nếu bạn muốn thay đổi _nội dung_ thực tế của commit cuối cùng của mình, quy trình hoạt động cơ bản theo cùng một cách -- trước tiên hãy thực hiện các thay đổi mà bạn nghĩ rằng bạn đã quên, tổ chức các thay đổi đó, và `git commit --amend` tiếp theo sẽ _thay thế_ commit cuối cùng đó bằng commit mới, được cải thiện của bạn. Bạn cần cẩn thận với kỹ thuật này vì việc sửa đổi (amending) sẽ thay đổi SHA-1 của commit. Nó giống như một rebase rất nhỏ -- đừng sửa đổi commit cuối cùng của bạn nếu bạn đã đẩy nó. [TIP] .Một commit đã sửa đổi có thể (hoặc không) cần một thông điệp commit đã sửa đổi ==== Khi bạn sửa đổi một commit, bạn có cơ hội thay đổi cả thông điệp commit và nội dung của commit. Nếu bạn sửa đổi nội dung của commit một cách đáng kể, bạn gần như chắc chắn nên cập nhật thông điệp commit để phản ánh nội dung đã sửa đổi đó. Mặt khác, nếu các sửa đổi của bạn là nhỏ nhặt (sửa lỗi đánh máy ngớ ngẩn hoặc thêm một tệp bạn quên tổ chức) sao cho thông điệp commit trước đó vẫn ổn, bạn có thể đơn giản thực hiện các thay đổi, tổ chức chúng, và tránh phiên soạn thảo không cần thiết hoàn toàn với: [source,console]
$ git commit --amend --no-edit
==== [[_changing_multiple]] ==== Thay đổi Nhiều Thông điệp Commit Để sửa đổi một commit nằm xa hơn trong lịch sử của bạn, bạn phải chuyển sang các công cụ phức tạp hơn. Git không có công cụ sửa đổi lịch sử, nhưng bạn có thể sử dụng công cụ rebase để rebase một loạt các commit lên HEAD mà chúng dựa trên ban đầu thay vì di chuyển chúng sang một cái khác. Với công cụ rebase tương tác, bạn có thể dừng lại sau mỗi commit bạn muốn sửa đổi và thay đổi thông điệp, thêm tệp, hoặc làm bất cứ điều gì bạn muốn. Bạn có thể chạy rebase một cách tương tác bằng cách thêm tùy chọn `-i` vào `git rebase`. Bạn phải chỉ ra bạn muốn viết lại các commit bao xa về phía sau bằng cách bảo lệnh commit nào để rebase lên. Ví dụ, nếu bạn muốn thay đổi ba thông điệp commit cuối cùng, hoặc bất kỳ thông điệp commit nào trong nhóm đó, bạn cung cấp làm đối số cho `git rebase -i` cha mẹ của commit cuối cùng bạn muốn chỉnh sửa, đó là `HEAD~2^` hoặc `HEAD~3`. Có thể dễ nhớ `~3` hơn vì bạn đang cố gắng chỉnh sửa ba commit cuối cùng, nhưng hãy nhớ rằng bạn thực sự đang chỉ định bốn commit trước đó, cha mẹ của commit cuối cùng bạn muốn chỉnh sửa: [source,console]
$ git rebase -i HEAD~3
Hãy nhớ lại rằng đây là một lệnh rebasing -- mọi commit trong khoảng `HEAD~3..HEAD` với một thông điệp đã thay đổi _và tất cả các hậu duệ của nó_ sẽ được viết lại. Đừng bao gồm bất kỳ commit nào bạn đã đẩy lên một máy chủ trung tâm -- làm như vậy sẽ gây nhầm lẫn cho các nhà phát triển khác bằng cách cung cấp một phiên bản thay thế của cùng một thay đổi. Chạy lệnh này cung cấp cho bạn một danh sách các commit trong trình soạn thảo văn bản của bạn trông giống như thế này: [source,console]
pick f7f3f6d Change my name a bit pick 310154e Update README formatting and add blame pick a5f4a0d Add cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8 # # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit # f, fixup <commit> = like "squash", but discard this commit’s log message # x, exec <command> = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop <commit> = remove commit # l, label <label> = label current HEAD with a name # t, reset <label> = reset HEAD to a label # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] # . create a merge commit using the original merge commit’s # . message (or the oneline, if no original merge commit was # . specified). Use -c <commit> to reword the commit message. # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
Điều quan trọng cần lưu ý là các commit này được liệt kê theo thứ tự ngược lại so với bạn thường thấy chúng sử dụng lệnh `log`. Nếu bạn chạy một `log`, bạn thấy một cái gì đó giống như thế này: [source,console]
$ git log --pretty=format:"%h %s" HEAD~3..HEAD a5f4a0d Add cat-file 310154e Update README formatting and add blame f7f3f6d Change my name a bit
Lưu ý thứ tự đảo ngược. Rebase tương tác cung cấp cho bạn một kịch bản mà nó sẽ chạy. Nó sẽ bắt đầu tại commit bạn chỉ định trên dòng lệnh (`HEAD~3`) và phát lại các thay đổi được giới thiệu trong mỗi commit này từ trên xuống dưới. Nó liệt kê cái cũ nhất ở trên cùng, thay vì cái mới nhất, bởi vì đó là cái đầu tiên nó sẽ phát lại. Bạn cần chỉnh sửa kịch bản để nó dừng lại tại commit bạn muốn chỉnh sửa. Để làm như vậy, hãy thay đổi từ "`pick`" thành từ "`edit`" cho mỗi commit bạn muốn kịch bản dừng lại sau đó. Ví dụ, để sửa đổi chỉ thông điệp commit thứ ba, bạn thay đổi tệp để trông giống như thế này: [source,console]
edit f7f3f6d Change my name a bit pick 310154e Update README formatting and add blame pick a5f4a0d Add cat-file
Khi bạn lưu và thoát trình soạn thảo, Git tua lại bạn trở lại commit cuối cùng trong danh sách đó và thả bạn xuống dòng lệnh với thông điệp sau: [source,console]
$ git rebase -i HEAD~3 Stopped at f7f3f6d… Change my name a bit You can amend the commit now, with
git commit --amend
Once you’re satisfied with your changes, run
git rebase --continue
Những hướng dẫn này cho bạn biết chính xác phải làm gì. Gõ: [source,console]
$ git commit --amend
Thay đổi thông điệp commit, và thoát trình soạn thảo. Sau đó, chạy: [source,console]
$ git rebase --continue
Lệnh này sẽ áp dụng hai commit khác một cách tự động, và sau đó bạn đã hoàn tất. Nếu bạn thay đổi `pick` thành `edit` trên nhiều dòng hơn, bạn có thể lặp lại các bước này cho mỗi commit bạn thay đổi thành `edit`. Mỗi lần, Git sẽ dừng lại, cho phép bạn sửa đổi commit, và tiếp tục khi bạn hoàn tất. ==== Sắp xếp lại Commit Bạn cũng có thể sử dụng rebase tương tác để sắp xếp lại hoặc xóa hoàn toàn các commit. Nếu bạn muốn xóa commit "`Add cat-file`" và thay đổi thứ tự mà hai commit khác được giới thiệu, bạn có thể thay đổi kịch bản rebase từ thế này: [source,console]
pick f7f3f6d Change my name a bit pick 310154e Update README formatting and add blame pick a5f4a0d Add cat-file
thành thế này: [source,console]
pick 310154e Update README formatting and add blame pick f7f3f6d Change my name a bit
Khi bạn lưu và thoát trình soạn thảo, Git tua lại nhánh của bạn về cha mẹ của các commit này, áp dụng `310154e` và sau đó `f7f3f6d`, và sau đó dừng lại. Bạn thay đổi hiệu quả thứ tự của các commit đó và xóa hoàn toàn commit "`Add cat-file`". [[_squashing]] ==== Gộp Commit (Squashing Commits) Cũng có thể lấy một loạt các commit và gộp (squash) chúng xuống thành một commit đơn lẻ với công cụ rebasing tương tác. Kịch bản đặt các hướng dẫn hữu ích trong thông điệp rebase: [source,console]
# # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit # f, fixup <commit> = like "squash", but discard this commit’s log message # x, exec <command> = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop <commit> = remove commit # l, label <label> = label current HEAD with a name # t, reset <label> = reset HEAD to a label # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] # . create a merge commit using the original merge commit’s # . message (or the oneline, if no original merge commit was # . specified). Use -c <commit> to reword the commit message. # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
Nếu, thay vì "`pick`" hoặc "`edit`", bạn chỉ định "`squash`", Git áp dụng cả thay đổi đó và thay đổi ngay trước nó và làm cho bạn hợp nhất các thông điệp commit lại với nhau. Vì vậy, nếu bạn muốn tạo một commit đơn lẻ từ ba commit này, bạn làm cho kịch bản trông giống như thế này: [source,console]
pick f7f3f6d Change my name a bit squash 310154e Update README formatting and add blame squash a5f4a0d Add cat-file
Khi bạn lưu và thoát trình soạn thảo, Git áp dụng tất cả ba thay đổi và sau đó đưa bạn trở lại trình soạn thảo để hợp nhất ba thông điệp commit: [source,console]
# This is a combination of 3 commits. # The first commit’s message is: Change my name a bit
# This is the 2nd commit message:
Update README formatting and add blame
# This is the 3rd commit message:
Add cat-file
Khi bạn lưu cái đó, bạn có một commit đơn lẻ giới thiệu các thay đổi của tất cả ba commit trước đó. ==== Tách một Commit Tách một commit hoàn tác một commit và sau đó tổ chức một phần và commit bao nhiêu lần tùy ý bạn muốn kết thúc. Ví dụ, giả sử bạn muốn tách commit ở giữa của ba commit của bạn. Thay vì "`Update README formatting and add blame`", bạn muốn tách nó thành hai commit: "`Update README formatting`" cho cái đầu tiên, và "`Add blame`" cho cái thứ hai. Bạn có thể làm điều đó trong kịch bản `rebase -i` bằng cách thay đổi hướng dẫn trên commit bạn muốn tách thành "`edit`": [source,console]
pick f7f3f6d Change my name a bit edit 310154e Update README formatting and add blame pick a5f4a0d Add cat-file
Sau đó, khi kịch bản thả bạn xuống dòng lệnh, bạn đặt lại (reset) commit đó, lấy các thay đổi đã được đặt lại, và tạo nhiều commit từ chúng. Khi bạn lưu và thoát trình soạn thảo, Git tua lại về cha mẹ của commit đầu tiên trong danh sách của bạn, áp dụng commit đầu tiên (`f7f3f6d`), áp dụng commit thứ hai (`310154e`), và thả bạn xuống bảng điều khiển. Ở đó, bạn có thể thực hiện một thiết lập lại hỗn hợp (mixed reset) của commit đó với `git reset HEAD^`, điều này hoàn tác hiệu quả commit đó và để lại các tệp đã sửa đổi chưa được tổ chức. Bây giờ bạn có thể tổ chức và commit các tệp cho đến khi bạn có một vài commit, và chạy `git rebase --continue` khi bạn hoàn tất: [source,console]
$ git reset HEAD^ $ git add README $ git commit -m 'Update README formatting' $ git add lib/simplegit.rb $ git commit -m 'Add blame' $ git rebase --continue
Git áp dụng commit cuối cùng (`a5f4a0d`) trong kịch bản, và lịch sử của bạn trông giống như thế này: [source,console]
$ git log -4 --pretty=format:"%h %s" 1c002dd Add cat-file 9b29157 Add blame 35cfb2b Update README formatting f7f3f6d Change my name a bit
Điều này thay đổi các SHA-1 của ba commit gần đây nhất trong danh sách của bạn, vì vậy hãy chắc chắn rằng không có commit đã thay đổi nào hiển thị trong danh sách đó mà bạn đã đẩy đến một kho lưu trữ chia sẻ. Lưu ý rằng commit cuối cùng (`f7f3f6d`) trong danh sách không thay đổi. Mặc dù commit này được hiển thị trong kịch bản, bởi vì nó được đánh dấu là "`pick`" và đã được áp dụng trước bất kỳ thay đổi rebase nào, Git để commit không bị sửa đổi. ==== Xóa một commit Nếu bạn muốn loại bỏ một commit, bạn có thể xóa nó bằng cách sử dụng kịch bản `rebase -i`. Trong danh sách các commit, đặt từ "`drop`" trước commit bạn muốn xóa (hoặc chỉ cần xóa dòng đó khỏi kịch bản rebase): [source,console]
pick 461cb2a This commit is OK drop 5aecc10 This commit is broken
Do cách Git xây dựng các đối tượng commit, việc xóa hoặc thay đổi một commit sẽ gây ra việc viết lại tất cả các commit theo sau nó. Bạn càng đi xa về phía sau trong lịch sử repo của mình, càng nhiều commit sẽ cần phải được tạo lại. Điều này có thể gây ra rất nhiều xung đột hợp nhất nếu bạn có nhiều commit sau đó trong chuỗi phụ thuộc vào cái bạn vừa xóa. Nếu bạn đi được một phần qua một rebase như thế này và quyết định đó không phải là một ý tưởng hay, bạn luôn có thể dừng lại. Gõ `git rebase --abort`, và repo của bạn sẽ được trả lại trạng thái trước khi bạn bắt đầu rebase. Nếu bạn hoàn thành một rebase và quyết định đó không phải là những gì bạn muốn, bạn có thể sử dụng `git reflog` để khôi phục một phiên bản cũ hơn của nhánh của bạn. Xem <<ch10-git-internals#_data_recovery>> để biết thêm thông tin về lệnh `reflog`. [NOTE] ==== Drew DeVault đã thực hiện một hướng dẫn thực hành thực tế với các bài tập để học cách sử dụng `git rebase`. Bạn có thể tìm thấy nó tại: https://git-rebase.io/[^] ==== ==== Phương án Hạt nhân: filter-branch Có một tùy chọn viết lại lịch sử khác mà bạn có thể sử dụng nếu bạn cần viết lại một số lượng lớn các commit theo một cách có thể viết kịch bản -- ví dụ, thay đổi địa chỉ email của bạn trên toàn cầu hoặc xóa một tệp khỏi mọi commit. Lệnh là `filter-branch`, và nó có thể viết lại những vùng lớn lịch sử của bạn, vì vậy bạn có lẽ không nên sử dụng nó trừ khi dự án của bạn chưa công khai và những người khác chưa dựa công việc vào các commit bạn sắp viết lại. Tuy nhiên, nó có thể rất hữu ích. Bạn sẽ tìm hiểu một vài cách sử dụng phổ biến để bạn có thể có ý tưởng về một số điều nó có khả năng làm. [CAUTION] ==== `git filter-branch` có nhiều cạm bẫy, và không còn là cách được khuyến nghị để viết lại lịch sử. Thay vào đó, hãy xem xét sử dụng `git-filter-repo`, là một kịch bản Python thực hiện công việc tốt hơn cho hầu hết các ứng dụng mà bạn thường chuyển sang `filter-branch`. Tài liệu và mã nguồn của nó có thể được tìm thấy tại https://github.com/newren/git-filter-repo[^]. ==== [[_removing_file_every_commit]] ===== Xóa một Tệp khỏi Mọi Commit Điều này xảy ra khá thường xuyên. Ai đó vô tình commit một tệp nhị phân khổng lồ với một lệnh `git add .` thiếu suy nghĩ, và bạn muốn xóa nó ở mọi nơi. Có lẽ bạn vô tình commit một tệp chứa mật khẩu, và bạn muốn làm cho dự án của mình thành mã nguồn mở. `filter-branch` là công cụ bạn có lẽ muốn sử dụng để làm sạch toàn bộ lịch sử của mình. Để xóa một tệp có tên `passwords.txt` khỏi toàn bộ lịch sử của bạn, bạn có thể sử dụng tùy chọn `--tree-filter` cho `filter-branch`: [source,console]
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21) Ref 'refs/heads/master' was rewritten
Tùy chọn `--tree-filter` chạy lệnh được chỉ định sau mỗi lần checkout của dự án và sau đó commit lại kết quả. Trong trường hợp này, bạn xóa một tệp gọi là `passwords.txt` khỏi mọi ảnh chụp nhanh, cho dù nó có tồn tại hay không. Nếu bạn muốn xóa tất cả các tệp sao lưu trình soạn thảo vô tình được commit, bạn có thể chạy một cái gì đó như `git filter-branch --tree-filter 'rm -f *~' HEAD`. Bạn sẽ có thể xem Git viết lại các cây và commit và sau đó di chuyển con trỏ nhánh ở cuối. Nói chung là một ý tưởng tốt để làm điều này trong một nhánh thử nghiệm và sau đó hard-reset nhánh `master` của bạn sau khi bạn đã xác định kết quả là những gì bạn thực sự muốn. Để chạy `filter-branch` trên tất cả các nhánh của bạn, bạn có thể truyền `--all` cho lệnh. ===== Làm cho một Thư mục con thành Gốc Mới Giả sử bạn đã thực hiện một lần nhập từ một hệ thống kiểm soát nguồn khác và có các thư mục con không có ý nghĩa (`trunk`, `tags`, v.v.). Nếu bạn muốn làm cho thư mục con `trunk` trở thành gốc dự án mới cho mọi commit, `filter-branch` cũng có thể giúp bạn làm điều đó: [source,console]
$ git filter-branch --subdirectory-filter trunk HEAD Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12) Ref 'refs/heads/master' was rewritten
Bây giờ gốc dự án mới của bạn là những gì đã ở trong thư mục con `trunk` mỗi lần. Git cũng sẽ tự động xóa các commit không ảnh hưởng đến thư mục con. ===== Thay đổi Địa chỉ Email Toàn cầu Một trường hợp phổ biến khác là bạn quên chạy `git config` để đặt tên và địa chỉ email của mình trước khi bạn bắt đầu làm việc, hoặc có lẽ bạn muốn mở mã nguồn một dự án tại nơi làm việc và thay đổi tất cả các địa chỉ email công việc của bạn thành địa chỉ cá nhân của bạn. Trong mọi trường hợp, bạn cũng có thể thay đổi địa chỉ email trong nhiều commit theo lô với `filter-branch`. Bạn cần cẩn thận chỉ thay đổi các địa chỉ email là của bạn, vì vậy bạn sử dụng `--commit-filter`: [source,console]
$ git filter-branch --commit-filter ' if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ]; then GIT_AUTHOR_NAME="Scott Chacon"; GIT_AUTHOR_EMAIL="schacon@example.com"; git commit-tree "$@"; else git commit-tree "$@"; fi' HEAD
Điều này đi qua và viết lại mọi commit để có địa chỉ mới của bạn. Bởi vì các commit chứa các giá trị SHA-1 của cha mẹ chúng, lệnh này thay đổi mọi SHA-1 commit trong lịch sử của bạn, không chỉ những cái có địa chỉ email phù hợp. [[_git_reset]] === Giải mã Reset (Reset Demystified) Trước khi chuyển sang các công cụ chuyên dụng hơn, hãy nói về các lệnh Git `reset` và `checkout`. Các lệnh này là hai trong số những phần khó hiểu nhất của Git khi bạn lần đầu tiên gặp chúng. Chúng làm rất nhiều việc đến nỗi dường như vô vọng để thực sự hiểu chúng và sử dụng chúng đúng cách. Đối với điều này, chúng tôi khuyên bạn nên dùng một phép ẩn dụ đơn giản. ==== Ba Cây (The Three Trees) Một cách dễ dàng hơn để suy nghĩ về `reset` và `checkout` là thông qua khung tư duy rằng Git là một trình quản lý nội dung của ba cây khác nhau. Bởi "`cây`" ở đây, chúng tôi thực sự có nghĩa là "`tập hợp các tệp`", không cụ thể là cấu trúc dữ liệu. Có một vài trường hợp chỉ mục không hoạt động chính xác như một cây, nhưng đối với mục đích của chúng ta, dễ dàng hơn để suy nghĩ về nó theo cách này ngay bây giờ. Git như một hệ thống quản lý và thao tác ba cây trong hoạt động bình thường của nó: [cols="1,2",options="header"] |================================ | Cây (Tree) | Vai trò | HEAD | Ảnh chụp nhanh commit cuối cùng, cha mẹ tiếp theo | Index (Chỉ mục) | Ảnh chụp nhanh commit tiếp theo được đề xuất | Working Directory (Thư mục Làm việc) | Hộp cát (Sandbox) |================================ ===== HEAD HEAD là con trỏ đến tham chiếu nhánh hiện tại, đến lượt nó là một con trỏ đến commit cuối cùng được thực hiện trên nhánh đó. Điều đó có nghĩa là HEAD sẽ là cha mẹ của commit tiếp theo được tạo ra. Nói chung là đơn giản nhất để nghĩ về HEAD như là ảnh chụp nhanh của *commit cuối cùng của bạn trên nhánh đó*. Trên thực tế, khá dễ dàng để xem ảnh chụp nhanh đó trông như thế nào. Dưới đây là một ví dụ về việc lấy danh sách thư mục thực tế và tổng kiểm tra SHA-1 cho mỗi tệp trong ảnh chụp nhanh HEAD: [source,console]
$ git cat-file -p HEAD tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf author Scott Chacon 1301511835 -0700 committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD 100644 blob a906cb2a4a904a152… README 100644 blob 8f94139338f9404f2… Rakefile 040000 tree 99f1a6d12cb4b6f19… lib
Các lệnh Git `cat-file` và `ls-tree` là các lệnh "`cấp thấp`" (plumbing) được sử dụng cho những việc ở cấp độ thấp hơn và không thực sự được sử dụng trong công việc hàng ngày, nhưng chúng giúp chúng ta thấy những gì đang diễn ra ở đây. [[_the_index]] ===== Chỉ mục (The Index) _Chỉ mục_ là *commit tiếp theo được đề xuất* của bạn. Chúng ta cũng đã đề cập đến khái niệm này là "`Khu vực Tổ chức`" (Staging Area) của Git vì đây là những gì Git nhìn vào khi bạn chạy `git commit`. Git điền vào chỉ mục này một danh sách tất cả các nội dung tệp đã được checkout lần cuối vào thư mục làm việc của bạn và chúng trông như thế nào khi chúng được checkout ban đầu. Sau đó, bạn thay thế một số tệp đó bằng các phiên bản mới của chúng, và `git commit` chuyển đổi nó thành cây cho một commit mới. [source,console]
$ git ls-files -s 100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README 100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile 100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb
Một lần nữa, ở đây chúng ta đang sử dụng `git ls-files`, đây là một lệnh hậu trường hiển thị cho bạn chỉ mục của bạn hiện trông như thế nào. Chỉ mục không phải là một cấu trúc cây về mặt kỹ thuật -- nó thực sự được triển khai như một bản kê khai phẳng -- nhưng đối với mục đích của chúng ta, nó đủ gần. ===== Thư mục Làm việc (The Working Directory) Cuối cùng, bạn có _thư mục làm việc_ của mình (cũng thường được gọi là "`cây làm việc`"). Hai cây kia lưu trữ nội dung của chúng theo một cách hiệu quả nhưng bất tiện, bên trong thư mục `.git`. Thư mục làm việc giải nén chúng thành các tệp thực tế, giúp bạn chỉnh sửa chúng dễ dàng hơn nhiều. Hãy nghĩ về thư mục làm việc như một *hộp cát*, nơi bạn có thể thử các thay đổi trước khi commit chúng vào khu vực tổ chức (chỉ mục) và sau đó vào lịch sử. [source,console]
$ tree . ├── README ├── Rakefile └── lib └── simplegit.rb
1 directory, 3 files
==== Quy trình làm việc (The Workflow) Quy trình làm việc điển hình của Git là ghi lại các ảnh chụp nhanh của dự án của bạn ở các trạng thái tốt hơn liên tiếp, bằng cách thao tác ba cây này. .Quy trình làm việc điển hình của Git image::images/reset-workflow.png[Quy trình làm việc điển hình của Git] Hãy hình dung quá trình này: giả sử bạn đi vào một thư mục mới với một tệp duy nhất trong đó. Chúng ta sẽ gọi đây là *v1* của tệp, và chúng ta sẽ chỉ ra nó bằng màu xanh lam. Bây giờ chúng ta chạy `git init`, sẽ tạo ra một kho lưu trữ Git với một tham chiếu HEAD trỏ đến nhánh `master` chưa sinh (unborn). .Kho lưu trữ Git mới được khởi tạo với tệp chưa được tổ chức trong thư mục làm việc image::images/reset-ex1.png[Kho lưu trữ Git mới được khởi tạo với tệp chưa được tổ chức trong thư mục làm việc] Tại thời điểm này, chỉ có cây thư mục làm việc có bất kỳ nội dung nào. Bây giờ chúng ta muốn commit tệp này, vì vậy chúng ta sử dụng `git add` để lấy nội dung trong thư mục làm việc và sao chép nó vào chỉ mục. .Tệp được sao chép vào chỉ mục khi `git add` image::images/reset-ex2.png[Tệp được sao chép vào chỉ mục khi `git add`] Sau đó, chúng ta chạy `git commit`, lấy nội dung của chỉ mục và lưu nó dưới dạng ảnh chụp nhanh vĩnh viễn, tạo một đối tượng commit trỏ đến ảnh chụp nhanh đó, và cập nhật `master` để trỏ đến commit đó. .Bước `git commit` image::images/reset-ex3.png[Bước `git commit`] Nếu chúng ta chạy `git status`, chúng ta sẽ không thấy thay đổi nào, bởi vì cả ba cây đều giống nhau. Bây giờ chúng ta muốn thực hiện một thay đổi cho tệp đó và commit nó. Chúng ta sẽ trải qua cùng một quy trình; đầu tiên, chúng ta thay đổi tệp trong thư mục làm việc của mình. Hãy gọi đây là *v2* của tệp, và chỉ ra nó bằng màu đỏ. .Kho lưu trữ Git với tệp đã thay đổi trong thư mục làm việc image::images/reset-ex4.png[Kho lưu trữ Git với tệp đã thay đổi trong thư mục làm việc] Nếu chúng ta chạy `git status` ngay bây giờ, chúng ta sẽ thấy tệp màu đỏ là "`Changes not staged for commit`" (Các thay đổi chưa được tổ chức để commit), bởi vì mục nhập đó khác nhau giữa chỉ mục và thư mục làm việc. Tiếp theo chúng ta chạy `git add` trên nó để tổ chức nó vào chỉ mục của chúng ta. .Tổ chức thay đổi vào chỉ mục image::images/reset-ex5.png[Tổ chức thay đổi vào chỉ mục] Tại thời điểm này, nếu chúng ta chạy `git status`, chúng ta sẽ thấy tệp màu xanh lục dưới "`Changes to be committed`" (Các thay đổi sẽ được commit) bởi vì chỉ mục và HEAD khác nhau -- nghĩa là, commit tiếp theo được đề xuất của chúng ta bây giờ khác với commit cuối cùng của chúng ta. Cuối cùng, chúng ta chạy `git commit` để hoàn tất commit. .Bước `git commit` với tệp đã thay đổi image::images/reset-ex6.png[Bước `git commit` với tệp đã thay đổi] Bây giờ `git status` sẽ không cung cấp cho chúng ta đầu ra nào, bởi vì cả ba cây lại giống nhau. Chuyển nhánh hoặc sao chép (cloning) cũng trải qua một quy trình tương tự. Khi bạn checkout một nhánh, nó thay đổi *HEAD* để trỏ đến tham chiếu nhánh mới, điền vào *chỉ mục* của bạn với ảnh chụp nhanh của commit đó, sau đó sao chép nội dung của *chỉ mục* vào *thư mục làm việc* của bạn. ==== Vai trò của Reset Lệnh `reset` có ý nghĩa hơn khi được xem trong bối cảnh này. Đối với mục đích của các ví dụ này, giả sử rằng chúng ta đã sửa đổi `file.txt` một lần nữa và commit nó lần thứ ba. Vì vậy, bây giờ lịch sử của chúng ta trông giống như thế này: .Kho lưu trữ Git với ba commit image::images/reset-start.png[Kho lưu trữ Git với ba commit] Bây giờ hãy xem xét chính xác những gì `reset` làm khi bạn gọi nó. Nó trực tiếp thao tác ba cây này theo một cách đơn giản và dễ đoán. Nó thực hiện tối đa ba hoạt động cơ bản. ===== Bước 1: Di chuyển HEAD Điều đầu tiên `reset` sẽ làm là di chuyển những gì HEAD trỏ đến. Điều này không giống như thay đổi chính HEAD (đó là những gì `checkout` làm); `reset` di chuyển nhánh mà HEAD đang trỏ đến. Điều này có nghĩa là nếu HEAD được đặt thành nhánh `master` (tức là bạn hiện đang ở trên nhánh `master`), chạy `git reset 9e5e6a4` sẽ bắt đầu bằng cách làm cho `master` trỏ đến `9e5e6a4`. .Soft reset (Reset mềm) image::images/reset-soft.png[Soft reset] Bất kể bạn gọi `reset` với một commit dưới hình thức nào, đây là điều đầu tiên nó sẽ luôn cố gắng làm. Với `reset --soft`, nó sẽ đơn giản dừng lại ở đó. Bây giờ hãy dành một giây để nhìn vào sơ đồ đó và nhận ra những gì đã xảy ra: về cơ bản nó đã hoàn tác lệnh `git commit` cuối cùng. Khi bạn chạy `git commit`, Git tạo ra một commit mới và di chuyển nhánh mà HEAD trỏ đến lên nó. Khi bạn `reset` trở lại `HEAD~` (cha mẹ của HEAD), bạn đang di chuyển nhánh trở lại nơi nó đã ở, mà không thay đổi chỉ mục hoặc thư mục làm việc. Bây giờ bạn có thể cập nhật chỉ mục và chạy `git commit` một lần nữa để hoàn thành những gì `git commit --amend` sẽ làm (xem <<_git_amend>>). ===== Bước 2: Cập nhật Chỉ mục (`--mixed`) Lưu ý rằng nếu bạn chạy `git status` bây giờ, bạn sẽ thấy màu xanh lục sự khác biệt giữa chỉ mục và HEAD mới là gì. Điều tiếp theo `reset` sẽ làm là cập nhật chỉ mục với nội dung của bất kỳ ảnh chụp nhanh nào mà HEAD hiện đang trỏ đến. .Mixed reset (Reset hỗn hợp) image::images/reset-mixed.png[Mixed reset] Nếu bạn chỉ định tùy chọn `--mixed`, `reset` sẽ dừng lại tại điểm này. Đây cũng là mặc định, vì vậy nếu bạn không chỉ định tùy chọn nào cả (chỉ `git reset HEAD~` trong trường hợp này), đây là nơi lệnh sẽ dừng lại. Bây giờ hãy dành một giây nữa để nhìn vào sơ đồ đó và nhận ra những gì đã xảy ra: nó vẫn hoàn tác `commit` cuối cùng của bạn, nhưng cũng _hủy tổ chức_ mọi thứ. Bạn đã quay trở lại trước khi bạn chạy tất cả các lệnh `git add` và `git commit` của mình. ===== Bước 3: Cập nhật Thư mục Làm việc (`--hard`) Điều thứ ba mà `reset` sẽ làm là làm cho thư mục làm việc trông giống như chỉ mục. Nếu bạn sử dụng tùy chọn `--hard`, nó sẽ tiếp tục đến giai đoạn này. .Hard reset (Reset cứng) image::images/reset-hard.png[Hard reset] Vì vậy, hãy suy nghĩ về những gì vừa xảy ra. Bạn đã hoàn tác commit cuối cùng của mình, các lệnh `git add` và `git commit`, *và* tất cả công việc bạn đã làm trong thư mục làm việc của mình. Điều quan trọng cần lưu ý là cờ này (`--hard`) là cách duy nhất để làm cho lệnh `reset` trở nên nguy hiểm, và là một trong số rất ít trường hợp Git sẽ thực sự phá hủy dữ liệu. Bất kỳ lời gọi nào khác của `reset` đều có thể được hoàn tác khá dễ dàng, nhưng tùy chọn `--hard` thì không thể, vì nó ghi đè lên các tệp trong thư mục làm việc một cách cưỡng bức. Trong trường hợp cụ thể này, chúng ta vẫn có phiên bản *v3* của tệp của mình trong một commit trong cơ sở dữ liệu Git của chúng ta, và chúng ta có thể lấy lại nó bằng cách xem `reflog` của mình, nhưng nếu chúng ta chưa commit nó, Git vẫn sẽ ghi đè lên tệp và nó sẽ không thể khôi phục được. ===== Tóm tắt (Recap) Lệnh `reset` ghi đè lên ba cây này theo một thứ tự cụ thể, dừng lại khi bạn bảo nó: 1. Di chuyển nhánh mà HEAD trỏ đến _(dừng ở đây nếu `--soft`)_. 2. Làm cho chỉ mục trông giống như HEAD _(dừng ở đây trừ khi `--hard`)_. 3. Làm cho thư mục làm việc trông giống như chỉ mục. ==== Reset Với một Đường dẫn Điều đó bao gồm hành vi của `reset` ở dạng cơ bản của nó, nhưng bạn cũng có thể cung cấp cho nó một đường dẫn để hành động. Nếu bạn chỉ định một đường dẫn, `reset` sẽ bỏ qua bước 1, và giới hạn phần còn lại của các hành động của nó đối với một tệp hoặc tập hợp các tệp cụ thể. Điều này thực sự có ý nghĩa -- HEAD chỉ là một con trỏ, và bạn không thể trỏ đến một phần của một commit và một phần của một commit khác. Nhưng chỉ mục và thư mục làm việc _có thể_ được cập nhật một phần, vì vậy reset tiến hành các bước 2 và 3. Vì vậy, giả sử chúng ta chạy `git reset file.txt`. Dạng này (vì bạn không chỉ định SHA-1 commit hoặc nhánh, và bạn không chỉ định `--soft` hoặc `--hard`) là viết tắt của `git reset --mixed HEAD file.txt`, sẽ: 1. Di chuyển nhánh mà HEAD trỏ đến _(bỏ qua)_. 2. Làm cho chỉ mục trông giống như HEAD _(dừng ở đây)_. Vì vậy, về cơ bản nó chỉ sao chép `file.txt` từ HEAD vào chỉ mục. .Mixed reset với một đường dẫn image::images/reset-path1.png[Mixed reset với một đường dẫn] Điều này có tác dụng thực tế là _hủy tổ chức_ tệp. Nếu chúng ta nhìn vào sơ đồ cho lệnh đó và suy nghĩ về những gì `git add` làm, chúng là những mặt đối lập chính xác. .Tổ chức tệp vào chỉ mục image::images/reset-path2.png[Tổ chức tệp vào chỉ mục] Đây là lý do tại sao đầu ra của lệnh `git status` gợi ý rằng bạn chạy lệnh này để hủy tổ chức một tệp (xem <<ch02-git-basics-chapter#_unstaging>> để biết thêm về điều này). Chúng ta cũng có thể dễ dàng không để Git giả định chúng ta có nghĩa là "`kéo dữ liệu từ HEAD`" bằng cách chỉ định một commit cụ thể để kéo phiên bản tệp đó từ đó. Chúng ta chỉ cần chạy một cái gì đó như `git reset eb43bf file.txt`. .Soft reset với một đường dẫn đến một commit cụ thể image::images/reset-path3.png[Soft reset với một đường dẫn đến một commit cụ thể] Điều này thực hiện hiệu quả điều tương tự như thể chúng ta đã hoàn nguyên nội dung của tệp về *v1* trong thư mục làm việc, chạy `git add` trên nó, sau đó hoàn nguyên nó trở lại *v3* một lần nữa (mà không thực sự trải qua tất cả các bước đó). Nếu chúng ta chạy `git commit` ngay bây giờ, nó sẽ ghi lại một thay đổi hoàn nguyên tệp đó trở lại *v1*, mặc dù chúng ta chưa bao giờ thực sự có nó trong thư mục làm việc của mình một lần nữa. Cũng thú vị khi lưu ý rằng giống như `git add`, lệnh `reset` sẽ chấp nhận tùy chọn `--patch` để hủy tổ chức nội dung trên cơ sở từng đoạn một (hunk-by-hunk). Vì vậy, bạn có thể hủy tổ chức hoặc hoàn nguyên nội dung một cách chọn lọc. ==== Gộp (Squashing) Hãy xem cách làm điều gì đó thú vị với sức mạnh mới tìm thấy này -- gộp các commit. Giả sử bạn có một loạt các commit với các thông điệp như "`oops.`", "`WIP`" và "`forgot this file`". Bạn có thể sử dụng `reset` để gộp chúng nhanh chóng và dễ dàng thành một commit đơn lẻ khiến bạn trông thực sự thông minh. <<_squashing>> chỉ ra một cách khác để làm điều này, nhưng trong ví dụ này, đơn giản hơn để sử dụng `reset`. Giả sử bạn có một dự án trong đó commit đầu tiên có một tệp, commit thứ hai đã thêm một tệp mới và thay đổi tệp đầu tiên, và commit thứ ba đã thay đổi tệp đầu tiên một lần nữa. Commit thứ hai là một công việc đang tiến hành và bạn muốn gộp nó xuống. .Kho lưu trữ Git image::images/reset-squash-r1.png[Kho lưu trữ Git] Bạn có thể chạy `git reset --soft HEAD~2` để di chuyển nhánh HEAD trở lại một commit cũ hơn (commit gần đây nhất bạn muốn giữ): .Di chuyển HEAD với soft reset image::images/reset-squash-r2.png[Di chuyển HEAD với soft reset] Và sau đó đơn giản chạy `git commit` một lần nữa: .Kho lưu trữ Git với commit đã gộp image::images/reset-squash-r3.png[Kho lưu trữ Git với commit đã gộp] Bây giờ bạn có thể thấy rằng lịch sử có thể truy cập của bạn, lịch sử bạn sẽ đẩy, bây giờ trông giống như bạn có một commit với `file-a.txt` *v1*, sau đó là một commit thứ hai vừa sửa đổi `file-a.txt` thành *v3* và thêm `file-b.txt`. Commit với phiên bản *v2* của tệp không còn trong lịch sử nữa. ==== Checkout (Check It Out) Cuối cùng, bạn có thể tự hỏi sự khác biệt giữa `checkout` và `reset` là gì. Giống như `reset`, `checkout` thao tác ba cây, và nó hơi khác một chút tùy thuộc vào việc bạn có cung cấp cho lệnh một đường dẫn tệp hay không. ===== Không có Đường dẫn Chạy `git checkout [nhánh]` khá giống với chạy `git reset --hard [nhánh]` ở chỗ nó cập nhật cả ba cây cho bạn để trông giống như `[nhánh]`, nhưng có hai sự khác biệt quan trọng. Đầu tiên, không giống như `reset --hard`, `checkout` an toàn cho thư mục làm việc; nó sẽ kiểm tra để đảm bảo nó không thổi bay các tệp có thay đổi đối với chúng. Thực ra, nó thông minh hơn thế một chút -- nó cố gắng thực hiện một hợp nhất tầm thường trong thư mục làm việc, vì vậy tất cả các tệp bạn _chưa_ thay đổi sẽ được cập nhật. `reset --hard`, mặt khác, sẽ đơn giản thay thế mọi thứ trên diện rộng mà không cần kiểm tra. Sự khác biệt quan trọng thứ hai là cách `checkout` cập nhật HEAD. Trong khi `reset` sẽ di chuyển nhánh mà HEAD trỏ đến, `checkout` sẽ di chuyển chính HEAD để trỏ đến một nhánh khác. Ví dụ, giả sử chúng ta có các nhánh `master` và `develop` trỏ đến các commit khác nhau, và chúng ta hiện đang ở trên `develop` (vì vậy HEAD trỏ đến nó). Nếu chúng ta chạy `git reset master`, bản thân `develop` bây giờ sẽ trỏ đến cùng một commit mà `master` làm. Nếu thay vào đó chúng ta chạy `git checkout master`, `develop` không di chuyển, chính HEAD di chuyển. HEAD bây giờ sẽ trỏ đến `master`. Vì vậy, trong cả hai trường hợp, chúng ta đang di chuyển HEAD để trỏ đến commit A, nhưng _cách_ chúng ta làm như vậy rất khác nhau. `reset` sẽ di chuyển nhánh mà HEAD trỏ đến, `checkout` di chuyển chính HEAD. .`git checkout` và `git reset` image::images/reset-checkout.png[`git checkout` và `git reset`] ===== Với Đường dẫn Cách khác để chạy `checkout` là với một đường dẫn tệp, giống như `reset`, không di chuyển HEAD. Nó giống như `git reset [nhánh] tệp` ở chỗ nó cập nhật chỉ mục với tệp đó tại commit đó, nhưng nó cũng ghi đè lên tệp trong thư mục làm việc. Nó sẽ chính xác giống như `git reset --hard [nhánh] tệp` (nếu `reset` cho phép bạn chạy điều đó) -- nó không an toàn cho thư mục làm việc, và nó không di chuyển HEAD. Ngoài ra, giống như `git reset` và `git add`, `checkout` sẽ chấp nhận tùy chọn `--patch` để cho phép bạn hoàn nguyên nội dung tệp một cách chọn lọc trên cơ sở từng đoạn một. ==== Tóm tắt Hy vọng bây giờ bạn hiểu và cảm thấy thoải mái hơn với lệnh `reset`, nhưng có lẽ vẫn còn một chút bối rối về việc nó khác chính xác như thế nào so với `checkout` và không thể nhớ tất cả các quy tắc của các lời gọi khác nhau. Dưới đây là một bảng ghi nhớ cho các lệnh nào ảnh hưởng đến các cây nào. Cột "`HEAD`" đọc là "`REF`" nếu lệnh đó di chuyển tham chiếu (nhánh) mà HEAD trỏ đến, và "`HEAD`" nếu nó di chuyển chính HEAD. Đặc biệt chú ý đến cột 'An toàn cho TMLV?' (WD Safe?) -- nếu nó nói *KHÔNG*, hãy dành một giây để suy nghĩ trước khi chạy lệnh đó. [options="header", cols="3,1,1,1,1"] |================================ | | HEAD | Index | Workdir | An toàn cho TMLV? | *Cấp độ Commit* | | | | | `reset --soft [commit]` | REF | KHÔNG | KHÔNG | CÓ | `reset [commit]` | REF | CÓ | KHÔNG | CÓ | `reset --hard [commit]` | REF | CÓ | CÓ | *KHÔNG* | `checkout <commit>` | HEAD | CÓ | CÓ | CÓ | *Cấp độ Tệp* | | | | | `reset [commit] <paths>` | KHÔNG | CÓ | KHÔNG | CÓ | `checkout [commit] <paths>` | KHÔNG | CÓ | CÓ | *KHÔNG* |================================ [[_advanced_merging]] === Hợp nhất Nâng cao Việc hợp nhất trong Git thường khá dễ dàng. Vì Git giúp dễ dàng hợp nhất một nhánh khác nhiều lần, điều đó có nghĩa là bạn có thể có một nhánh sống rất lâu nhưng bạn có thể giữ nó được cập nhật khi bạn đi, giải quyết các xung đột nhỏ thường xuyên, thay vì bị bất ngờ bởi một xung đột khổng lồ vào cuối chuỗi. Tuy nhiên, đôi khi các xung đột phức tạp vẫn xảy ra. Không giống như một số hệ thống kiểm soát phiên bản khác, Git không cố gắng tỏ ra quá thông minh về việc giải quyết xung đột hợp nhất. Triết lý của Git là thông minh trong việc xác định khi nào một giải pháp hợp nhất là không mơ hồ, nhưng nếu có xung đột, nó không cố gắng tỏ ra thông minh về việc tự động giải quyết nó. Do đó, nếu bạn đợi quá lâu để hợp nhất hai nhánh phân kỳ nhanh chóng, bạn có thể gặp phải một số vấn đề. Trong phần này, chúng ta sẽ xem xét một số vấn đề đó có thể là gì và những công cụ nào Git cung cấp cho bạn để giúp xử lý những tình huống phức tạp hơn này. Chúng ta cũng sẽ đề cập đến một số loại hợp nhất khác nhau, không tiêu chuẩn mà bạn có thể thực hiện, cũng như xem cách rút lại các lần hợp nhất bạn đã thực hiện. ==== Xung đột Hợp nhất Mặc dù chúng ta đã đề cập đến một số điều cơ bản về việc giải quyết xung đột hợp nhất trong <<ch03-git-branching#_basic_merge_conflicts>>, đối với các xung đột phức tạp hơn, Git cung cấp một vài công cụ để giúp bạn tìm hiểu những gì đang xảy ra và cách xử lý xung đột tốt hơn. Trước hết, nếu có thể, hãy cố gắng đảm bảo thư mục làm việc của bạn sạch sẽ trước khi thực hiện một hợp nhất có thể có xung đột. Nếu bạn có công việc đang dang dở, hãy cam kết nó vào một nhánh tạm thời hoặc cất giữ nó. Điều này giúp bạn có thể hoàn tác _bất cứ điều gì_ bạn thử ở đây. Nếu bạn có những thay đổi chưa được lưu trong thư mục làm việc của mình khi bạn thử một hợp nhất, một số mẹo này có thể giúp bạn bảo tồn công việc đó. Hãy cùng xem một ví dụ rất đơn giản. Chúng ta có một tệp Ruby siêu đơn giản in ra 'hello world'. [source,ruby]
#! /usr/bin/env ruby
def hello puts 'hello world' end
hello()
Trong kho lưu trữ của chúng ta, chúng ta tạo một nhánh mới có tên `whitespace` và tiến hành thay đổi tất cả các kết thúc dòng Unix thành kết thúc dòng DOS, về cơ bản là thay đổi mọi dòng của tệp, nhưng chỉ với khoảng trắng. Sau đó, chúng ta thay đổi dòng "`hello world`" thành "`hello mundo`". [source,console]
$ git checkout -b whitespace Switched to a new branch 'whitespace'
$ unix2dos hello.rb unix2dos: converting file hello.rb to DOS format … $ git commit -am 'Convert hello.rb to DOS' [whitespace 3270f76] Convert hello.rb to DOS 1 file changed, 7 insertions(+), 7 deletions(-)
$ vim hello.rb $ git diff -b diff --git a/hello.rb b/hello.rb index ac51efd..e85207e 100755 --- a/hello.rb + b/hello.rb @@ -1,7 +1,7 @@ #! /usr/bin/env ruby
def hello - puts 'hello world' + puts 'hello mundo'^M end
hello()
$ git commit -am 'Use Spanish instead of English' [whitespace 6d338d2] Use Spanish instead of English 1 file changed, 1 insertion(+), 1 deletion(-)
Bây giờ chúng ta chuyển về nhánh `master` của mình và thêm một số tài liệu cho hàm. [source,console]
$ git checkout master Switched to branch 'master'
$ vim hello.rb $ git diff diff --git a/hello.rb b/hello.rb index ac51efd..36c06c8 100755 --- a/hello.rb + b/hello.rb @@ -1,5 +1,6 @@ #! /usr/bin/env ruby
+# prints out a greeting def hello puts 'hello world' end
$ git commit -am 'Add comment documenting the function' [master bec6336] Add comment documenting the function 1 file changed, 1 insertion(+)
Bây giờ chúng ta cố gắng hợp nhất nhánh `whitespace` của mình và chúng ta sẽ gặp xung đột vì các thay đổi khoảng trắng. [source,console]
$ git merge whitespace Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Automatic merge failed; fix conflicts and then commit the result.
[[_abort_merge]] ===== Hủy bỏ một Hợp nhất Bây giờ chúng ta có một vài lựa chọn. Đầu tiên, hãy xem cách thoát khỏi tình huống này. Nếu có lẽ bạn không mong đợi xung đột và chưa muốn giải quyết tình hình ngay lập tức, bạn có thể chỉ cần rút lại việc hợp nhất bằng `git merge --abort`. [source,console]
$ git status -sb ## master UU hello.rb
$ git merge --abort
$ git status -sb ## master
Tùy chọn `git merge --abort` cố gắng hoàn nguyên về trạng thái của bạn trước khi bạn chạy hợp nhất. Những trường hợp duy nhất mà nó có thể không thể làm điều này một cách hoàn hảo là nếu bạn có những thay đổi chưa được cất giữ, chưa được cam kết trong thư mục làm việc của mình khi bạn chạy nó, nếu không thì nó sẽ hoạt động tốt. Nếu vì lý do nào đó bạn chỉ muốn bắt đầu lại, bạn cũng có thể chạy `git reset --hard HEAD`, và kho lưu trữ của bạn sẽ trở lại trạng thái đã cam kết cuối cùng. Hãy nhớ rằng bất kỳ công việc chưa được cam kết nào cũng sẽ bị mất, vì vậy hãy chắc chắn rằng bạn không muốn bất kỳ thay đổi nào của mình. ===== Bỏ qua Khoảng trắng Trong trường hợp cụ thể này, các xung đột liên quan đến khoảng trắng. Chúng ta biết điều này bởi vì trường hợp này đơn giản, nhưng cũng khá dễ dàng để nhận biết trong các trường hợp thực tế khi xem xét xung đột vì mọi dòng đều bị xóa ở một bên và được thêm lại ở bên kia. Theo mặc định, Git thấy tất cả các dòng này đã bị thay đổi, vì vậy nó không thể hợp nhất các tệp. Tuy nhiên, chiến lược hợp nhất mặc định có thể nhận các đối số, và một vài trong số đó là về việc bỏ qua các thay đổi khoảng trắng một cách hợp lý. Nếu bạn thấy rằng bạn có nhiều vấn đề về khoảng trắng trong một hợp nhất, bạn có thể chỉ cần hủy bỏ nó và thực hiện lại, lần này với `-Xignore-all-space` hoặc `-Xignore-space-change`. Tùy chọn đầu tiên bỏ qua khoảng trắng _hoàn toàn_ khi so sánh các dòng, tùy chọn thứ hai coi các chuỗi của một hoặc nhiều ký tự khoảng trắng là tương đương. [source,console]
$ git merge -Xignore-space-change whitespace Auto-merging hello.rb Merge made by the 'recursive' strategy. hello.rb | 2 - 1 file changed, 1 insertion(), 1 deletion(-)
Vì trong trường hợp này, các thay đổi tệp thực tế không xung đột, một khi chúng ta bỏ qua các thay đổi khoảng trắng, mọi thứ sẽ hợp nhất tốt.
Đây là một cứu cánh nếu bạn có ai đó trong nhóm của mình thỉnh thoảng thích định dạng lại mọi thứ từ dấu cách thành tab hoặc ngược lại.
[[_manual_remerge]]
===== Hợp nhất lại Tệp Thủ công
Mặc dù Git xử lý việc tiền xử lý khoảng trắng khá tốt, nhưng có những loại thay đổi khác mà có lẽ Git không thể xử lý tự động, nhưng là các bản sửa lỗi có thể lập trình được.
Ví dụ, hãy giả vờ rằng Git không thể xử lý thay đổi khoảng trắng và chúng ta cần phải làm điều đó bằng tay.
Những gì chúng ta thực sự cần làm là chạy tệp chúng ta đang cố gắng hợp nhất qua một chương trình `dos2unix` trước khi thử hợp nhất tệp thực tế.
Vậy làm thế nào chúng ta có thể làm điều đó?
Đầu tiên, chúng ta vào trạng thái xung đột hợp nhất.
Sau đó, chúng ta muốn lấy các bản sao của phiên bản tệp của mình, phiên bản của họ (từ nhánh chúng ta đang hợp nhất) và phiên bản chung (từ nơi cả hai bên phân nhánh).
Sau đó, chúng ta muốn sửa chữa bên của họ hoặc bên của chúng ta và thử lại việc hợp nhất chỉ cho tệp đơn này.
Việc lấy ba phiên bản tệp thực sự khá dễ dàng.
Git lưu trữ tất cả các phiên bản này trong chỉ mục dưới dạng "`stages`" mà mỗi phiên bản đều có các số được liên kết với chúng.
Giai đoạn 1 là tổ tiên chung, giai đoạn 2 là phiên bản của bạn và giai đoạn 3 là từ `MERGE_HEAD`, phiên bản bạn đang hợp nhất ("`của họ`").
Bạn có thể trích xuất một bản sao của mỗi phiên bản của tệp xung đột này bằng lệnh `git show` và một cú pháp đặc biệt.
[source,console]
$ git show :1:hello.rb > hello.common.rb $ git show :2:hello.rb > hello.ours.rb $ git show :3:hello.rb > hello.theirs.rb
Nếu bạn muốn hardcore hơn một chút, bạn cũng có thể sử dụng lệnh `ls-files -u` để lấy các SHA-1 thực tế của các blob Git cho mỗi tệp này. [source,console]
$ git ls-files -u 100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1\thello.rb 100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2\thello.rb 100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3\thello.rb
`:1:hello.rb` chỉ là một cách viết tắt để tra cứu SHA-1 blob đó. Bây giờ chúng ta đã có nội dung của cả ba giai đoạn trong thư mục làm việc của mình, chúng ta có thể sửa chữa thủ công của họ để khắc phục sự cố khoảng trắng và hợp nhất lại tệp bằng lệnh `git merge-file` ít được biết đến, lệnh này làm chính xác điều đó. [source,console]
$ dos2unix hello.theirs.rb dos2unix: converting file hello.theirs.rb to Unix format …
$ git merge-file -p \ hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb
$ git diff -b diff --cc hello.rb index 36c06c8,e85207e..0000000 --- a/hello.rb + b/hello.rb @@@ -1,8 -1,7 +1,8 @@@ #! /usr/bin/env ruby
+# prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end
hello()
Tại thời điểm này, chúng ta đã hợp nhất tệp một cách tốt đẹp. Trên thực tế, điều này hoạt động tốt hơn tùy chọn `ignore-space-change` vì điều này thực sự khắc phục các thay đổi khoảng trắng trước khi hợp nhất thay vì chỉ bỏ qua chúng. Trong hợp nhất `ignore-space-change`, chúng ta thực sự đã kết thúc với một vài dòng có kết thúc dòng DOS, làm cho mọi thứ bị lẫn lộn. Nếu bạn muốn có một ý tưởng trước khi hoàn tất cam kết này về những gì đã thực sự thay đổi giữa một bên hoặc bên kia, bạn có thể yêu cầu `git diff` so sánh những gì có trong thư mục làm việc của bạn mà bạn sắp cam kết là kết quả của việc hợp nhất với bất kỳ giai đoạn nào trong số này. Hãy cùng xem tất cả chúng. Để so sánh kết quả của bạn với những gì bạn đã có trong nhánh của mình trước khi hợp nhất, nói cách khác, để xem những gì việc hợp nhất đã giới thiệu, bạn có thể chạy `git diff --ours`: [source,console]
$ git diff --ours * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index 36c06c8..44d0a25 100755 --- a/hello.rb + b/hello.rb @@ -2,7 +2,7 @@
# prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end
hello()
Vì vậy, ở đây chúng ta có thể dễ dàng thấy rằng những gì đã xảy ra trong nhánh của chúng ta, những gì chúng ta thực sự đang giới thiệu vào tệp này với lần hợp nhất này, là thay đổi dòng đơn đó. Nếu chúng ta muốn xem kết quả của việc hợp nhất khác với những gì ở phía họ như thế nào, bạn có thể chạy `git diff --theirs`. Trong ví dụ này và ví dụ sau, chúng ta phải sử dụng `-b` để loại bỏ khoảng trắng vì chúng ta đang so sánh nó với những gì có trong Git, chứ không phải tệp `hello.theirs.rb` đã được làm sạch của chúng ta. [source,console]
$ git diff --theirs -b * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index e85207e..44d0a25 100755 --- a/hello.rb + b/hello.rb @@ -1,5 +1,6 @@ #! /usr/bin/env ruby
+# prints out a greeting def hello puts 'hello mundo' end
Cuối cùng, bạn có thể xem tệp đã thay đổi như thế nào từ cả hai phía với `git diff --base`. [source,console]
$ git diff --base -b * Unmerged path hello.rb diff --git a/hello.rb b/hello.rb index ac51efd..44d0a25 100755 --- a/hello.rb + b/hello.rb @@ -1,7 +1,8 @@ #! /usr/bin/env ruby
+# prints out a greeting def hello - puts 'hello world' + puts 'hello mundo' end
hello()
Tại thời điểm này, chúng ta có thể sử dụng lệnh `git clean` để dọn dẹp các tệp bổ sung mà chúng ta đã tạo để thực hiện việc hợp nhất thủ công nhưng không còn cần nữa. [source,console]
$ git clean -f Removing hello.common.rb Removing hello.ours.rb Removing hello.theirs.rb
[[_checking_out_conflicts]] ===== Kiểm tra Xung đột Có lẽ chúng ta không hài lòng với giải pháp tại thời điểm này vì lý do nào đó, hoặc có thể việc chỉnh sửa thủ công một hoặc cả hai bên vẫn không hoạt động tốt và chúng ta cần thêm ngữ cảnh. Hãy thay đổi ví dụ một chút. Đối với ví dụ này, chúng ta có hai nhánh sống lâu hơn, mỗi nhánh có một vài cam kết trong đó nhưng tạo ra một xung đột nội dung hợp pháp khi được hợp nhất. [source,console]
$ git log --graph --oneline --decorate --all * f1270f7 (HEAD, master) Update README * 9af9d3b Create README * 694971d Update phrase to 'hola world' | * e3eb223 (mundo) Add more tests | * 7cff591 Create initial testing script | * c3ffff1 Change text to 'hello mundo' |/ * b7dcc89 Initial hello world code
Bây giờ chúng ta có ba cam kết duy nhất chỉ tồn tại trên nhánh `master` và ba cam kết khác tồn tại trên nhánh `mundo`. Nếu chúng ta cố gắng hợp nhất nhánh `mundo` vào, chúng ta sẽ gặp xung đột. [source,console]
$ git merge mundo Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Automatic merge failed; fix conflicts and then commit the result.
Chúng ta muốn xem xung đột hợp nhất là gì. Nếu chúng ta mở tệp, chúng ta sẽ thấy một cái gì đó như thế này: [source,ruby]
#! /usr/bin/env ruby
def hello <<<<<<< HEAD puts 'hola world'
puts 'hello mundo' >>>>>>> mundo end
hello()
Cả hai bên của việc hợp nhất đều đã thêm nội dung vào tệp này, nhưng một số cam kết đã sửa đổi tệp ở cùng một nơi gây ra xung đột này. Hãy khám phá một vài công cụ mà bạn hiện có để xác định xung đột này đã xảy ra như thế nào. Có lẽ không rõ ràng chính xác bạn nên khắc phục xung đột này như thế nào. Bạn cần thêm ngữ cảnh. Một công cụ hữu ích là `git checkout` với tùy chọn `--conflict`. Điều này sẽ kiểm tra lại tệp và thay thế các dấu xung đột hợp nhất. Điều này có thể hữu ích nếu bạn muốn đặt lại các dấu và thử giải quyết chúng lại. Bạn có thể chuyển `--conflict` hoặc `diff3` hoặc `merge` (là mặc định). Nếu bạn chuyển nó `diff3`, Git sẽ sử dụng một phiên bản hơi khác của các dấu xung đột, không chỉ cung cấp cho bạn các phiên bản "`của chúng ta`" và "`của họ`", mà còn cả phiên bản "`cơ sở`" cùng dòng để cung cấp cho bạn nhiều ngữ cảnh hơn. [source,console]
$ git checkout --conflict=diff3 hello.rb
Sau khi chúng ta chạy lệnh đó, tệp sẽ trông như thế này thay thế: [source,ruby]
#! /usr/bin/env ruby
def hello <<<<<<< ours puts 'hola world' ||||||| base puts 'hello world'
puts 'hello mundo' >>>>>>> theirs end
hello()
Nếu bạn thích định dạng này, bạn có thể đặt nó làm mặc định cho các xung đột hợp nhất trong tương lai bằng cách đặt cài đặt `merge.conflictstyle` thành `diff3`. [source,console]
$ git config --global merge.conflictstyle diff3
Lệnh `git checkout` cũng có thể nhận các tùy chọn `--ours` và `--theirs`, đây có thể là một cách rất nhanh để chỉ chọn một bên hoặc bên kia mà không cần hợp nhất mọi thứ. Điều này có thể đặc biệt hữu ích đối với các xung đột của các tệp nhị phân nơi bạn có thể chỉ cần chọn một bên, hoặc nơi bạn chỉ muốn hợp nhất một số tệp nhất định từ một nhánh khác -- bạn có thể thực hiện việc hợp nhất và sau đó kiểm tra một số tệp nhất định từ một bên hoặc bên kia trước khi cam kết. [[_merge_log]] ===== Nhật ký Hợp nhất Một công cụ hữu ích khác khi giải quyết xung đột hợp nhất là `git log`. Điều này có thể giúp bạn có được ngữ cảnh về những gì có thể đã góp phần vào các xung đột. Xem lại một chút lịch sử để nhớ tại sao hai dòng phát triển lại chạm vào cùng một khu vực mã đôi khi có thể rất hữu ích. Để có được danh sách đầy đủ tất cả các cam kết duy nhất đã được bao gồm trong cả hai nhánh liên quan đến việc hợp nhất này, chúng ta có thể sử dụng cú pháp "`ba chấm`" mà chúng ta đã học trong <<ch07-git-tools#_triple_dot>>. [source,console]
$ git log --oneline --left-right HEAD…MERGE_HEAD < f1270f7 Update README < 9af9d3b Create README < 694971d Update phrase to 'hola world' > e3eb223 Add more tests > 7cff591 Create initial testing script > c3ffff1 Change text to 'hello mundo'
Đó là một danh sách tốt của sáu cam kết tổng cộng có liên quan, cũng như dòng phát triển nào mà mỗi cam kết thuộc về. Tuy nhiên, chúng ta có thể đơn giản hóa điều này hơn nữa để cung cấp cho chúng ta ngữ cảnh cụ thể hơn nhiều. Nếu chúng ta thêm tùy chọn `--merge` vào `git log`, nó sẽ chỉ hiển thị các cam kết ở cả hai bên của việc hợp nhất chạm vào một tệp hiện đang bị xung đột. [source,console]
$ git log --oneline --left-right --merge < 694971d Update phrase to 'hola world' > c3ffff1 Change text to 'hello mundo'
Nếu bạn chạy lệnh đó với tùy chọn `-p`, bạn chỉ nhận được các diff cho tệp đã kết thúc trong xung đột. Điều này có thể _thực sự_ hữu ích trong việc nhanh chóng cung cấp cho bạn ngữ cảnh bạn cần để giúp hiểu tại sao một cái gì đó xung đột và cách giải quyết nó một cách thông minh hơn. ===== Định dạng Diff Kết hợp Vì Git tổ chức bất kỳ kết quả hợp nhất nào thành công, khi bạn chạy `git diff` trong khi ở trạng thái hợp nhất bị xung đột, bạn chỉ nhận được những gì hiện vẫn còn trong xung đột. Điều này có thể hữu ích để xem bạn vẫn còn phải giải quyết những gì. Khi bạn chạy `git diff` ngay sau một xung đột hợp nhất, nó sẽ cung cấp cho bạn thông tin ở một định dạng đầu ra diff khá độc đáo. [source,console]
$ git diff diff --cc hello.rb index 0399cd5,59727f0..0000000 --- a/hello.rb + b/hello.rb @@@ -1,7 -1,7 +1,11 @@@ #! /usr/bin/env ruby
def hello ++<<<<<<< HEAD + puts 'hola world' ++======= + puts 'hello mundo' ++>>>>>>> mundo end
hello()
Định dạng này được gọi là "`Diff Kết hợp`" và cung cấp cho bạn hai cột dữ liệu bên cạnh mỗi dòng. Cột đầu tiên cho bạn biết dòng đó có khác biệt (được thêm hoặc xóa) giữa nhánh "`của chúng ta`" và tệp trong thư mục làm việc của bạn hay không và cột thứ hai làm điều tương tự giữa nhánh "`của họ`" và bản sao thư mục làm việc của bạn. Vì vậy, trong ví dụ đó, bạn có thể thấy rằng các dòng `<<<<<<<` và `>>>>>>>` có trong bản sao làm việc nhưng không có ở cả hai bên của việc hợp nhất. Điều này có ý nghĩa vì công cụ hợp nhất đã dán chúng vào đó để cung cấp ngữ cảnh cho chúng ta, nhưng chúng ta được mong đợi sẽ xóa chúng. Nếu chúng ta giải quyết xung đột và chạy lại `git diff`, chúng ta sẽ thấy điều tương tự, nhưng nó hữu ích hơn một chút. [source,console]
$ vim hello.rb $ git diff diff --cc hello.rb index 0399cd5,59727f0..0000000 --- a/hello.rb + b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby
def hello - puts 'hola world' - puts 'hello mundo' + puts 'hola mundo' end
hello()
Điều này cho chúng ta thấy rằng "`hola world`" có ở phía chúng ta nhưng không có trong bản sao làm việc, rằng "`hello mundo`" có ở phía họ nhưng không có trong bản sao làm việc và cuối cùng là "`hola mundo`" không có ở cả hai bên nhưng bây giờ có trong bản sao làm việc. Điều này có thể hữu ích để xem xét trước khi cam kết giải pháp. Bạn cũng có thể lấy điều này từ `git log` cho bất kỳ hợp nhất nào để xem một cái gì đó đã được giải quyết như thế nào sau đó. Git sẽ xuất ra định dạng này nếu bạn chạy `git show` trên một cam kết hợp nhất, hoặc nếu bạn thêm tùy chọn `--cc` vào một `git log -p` (mà theo mặc định chỉ hiển thị các bản vá cho các cam kết không phải là hợp nhất). [source,console]
$ git log --cc -p -1 commit 14f41939956d80b9e17bb8721354c33f8d5b5a79 Merge: f1270f7 e3eb223 Author: Scott Chacon <schacon@gmail.com> Date: Fri Sep 19 18:14:49 2014 +0200
Merge branch 'mundo'
Conflicts:
hello.rb
diff --cc hello.rb index 0399cd5,59727f0..e1d0799 --- a/hello.rb + b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby
def hello - puts 'hola world' - puts 'hello mundo' + puts 'hola mundo' end
hello()
[[_undoing_merges]] ==== Hoàn tác các Hợp nhất Bây giờ bạn đã biết cách tạo một cam kết hợp nhất, bạn có thể sẽ mắc phải một số sai lầm. Một trong những điều tuyệt vời khi làm việc với Git là bạn có thể mắc sai lầm, bởi vì có thể (và trong nhiều trường hợp là dễ dàng) để sửa chữa chúng. Các cam kết hợp nhất cũng không khác. Giả sử bạn bắt đầu làm việc trên một nhánh chủ đề, vô tình hợp nhất nó vào `master`, và bây giờ lịch sử cam kết của bạn trông như thế này: .Cam kết hợp nhất vô tình image::images/undomerge-start.png[Cam kết hợp nhất vô tình] Có hai cách để tiếp cận vấn đề này, tùy thuộc vào kết quả mong muốn của bạn. ===== Sửa các tham chiếu Nếu cam kết hợp nhất không mong muốn chỉ tồn tại trên kho lưu trữ cục bộ của bạn, giải pháp dễ nhất và tốt nhất là di chuyển các nhánh để chúng trỏ đến nơi bạn muốn. Trong hầu hết các trường hợp, nếu bạn theo sau `git merge` sai lầm bằng `git reset --hard HEAD~`, điều này sẽ đặt lại các con trỏ nhánh để chúng trông như thế này: .Lịch sử sau `git reset --hard HEAD~` image::images/undomerge-reset.png[Lịch sử sau `git reset --hard HEAD~`] Chúng ta đã đề cập đến `reset` trong <<ch07-git-tools#_git_reset>>, vì vậy không quá khó để hiểu những gì đang xảy ra ở đây. Đây là một bản tóm tắt nhanh: `reset --hard` thường trải qua ba bước: . Di chuyển nhánh mà HEAD đang trỏ đến. Trong trường hợp này, chúng ta muốn di chuyển `master` đến nơi nó đã ở trước khi cam kết hợp nhất (`C6`). . Làm cho chỉ mục trông giống như HEAD. . Làm cho thư mục làm việc trông giống như chỉ mục. Nhược điểm của phương pháp này là nó đang viết lại lịch sử, điều này có thể gây ra vấn đề với một kho lưu trữ được chia sẻ. Hãy xem <<ch03-git-branching#_rebase_peril>> để biết thêm về những gì có thể xảy ra; phiên bản ngắn gọn là nếu những người khác có các cam kết bạn đang viết lại, bạn có lẽ nên tránh `reset`. Phương pháp này cũng sẽ không hoạt động nếu bất kỳ cam kết nào khác đã được tạo ra kể từ khi hợp nhất; việc di chuyển các tham chiếu sẽ thực sự làm mất các thay đổi đó. [[_reverse_commit]] ===== Hoàn nguyên cam kết Nếu việc di chuyển các con trỏ nhánh không phù hợp với bạn, Git cung cấp cho bạn tùy chọn tạo một cam kết mới hoàn tác tất cả các thay đổi từ một cam kết hiện có. Git gọi hoạt động này là "`hoàn nguyên`", và trong kịch bản cụ thể này, bạn sẽ gọi nó như thế này: [source,console]
$ git revert -m 1 HEAD [master b1d8379] Revert "Merge branch 'topic'"
Cờ `-m 1` cho biết cha mẹ nào là "`dòng chính`" và nên được giữ lại. Khi bạn gọi một hợp nhất vào `HEAD` (`git merge topic`), cam kết mới có hai cha mẹ: cha mẹ đầu tiên là `HEAD` (`C6`), và cha mẹ thứ hai là đầu của nhánh đang được hợp nhất (`C4`). Trong trường hợp này, chúng ta muốn hoàn tác tất cả các thay đổi được giới thiệu bằng cách hợp nhất vào cha mẹ #2 (`C4`), trong khi vẫn giữ tất cả nội dung từ cha mẹ #1 (`C6`). Lịch sử với cam kết hoàn nguyên trông như thế này: .Lịch sử sau `git revert -m 1` image::images/undomerge-revert.png[Lịch sử sau `git revert -m 1`] Cam kết mới `^M` có nội dung hoàn toàn giống với `C6`, vì vậy bắt đầu từ đây, nó giống như việc hợp nhất chưa bao giờ xảy ra, ngoại trừ việc các cam kết hiện chưa được hợp nhất vẫn còn trong lịch sử của ``HEAD```. Git sẽ bị rối nếu bạn cố gắng hợp nhất ``topic`` vào ``master`` một lần nữa: [source,console]
$ git merge topic Already up-to-date.
Không có gì trong `topic` mà không thể truy cập được từ `master`. Tệ hơn nữa, nếu bạn thêm công việc vào `topic` và hợp nhất lại, Git sẽ chỉ mang vào các thay đổi _kể từ_ lần hợp nhất bị hoàn nguyên: .Lịch sử với một hợp nhất tồi image::images/undomerge-revert2.png[Lịch sử với một hợp nhất tồi] Cách tốt nhất để giải quyết vấn đề này là hoàn tác lại lần hợp nhất ban đầu, vì bây giờ bạn muốn mang vào các thay đổi đã bị hoàn nguyên, *sau đó* tạo một cam kết hợp nhất mới: [source,console]
$ git revert ^M [master 09f0126] Revert "Revert \"Merge branch 'topic'\"" $ git merge topic
.Lịch sử sau khi hợp nhất lại một hợp nhất bị hoàn nguyên image::images/undomerge-revert3.png[Lịch sử sau khi hợp nhất lại một hợp nhất bị hoàn nguyên] Trong ví dụ này, `M` và `^M` triệt tiêu nhau. `^^M` thực sự hợp nhất các thay đổi từ `C3` và `C4`, và `C8` hợp nhất các thay đổi từ `C7`, vì vậy bây giờ `topic` đã được hợp nhất hoàn toàn. ==== Các Loại Hợp nhất Khác Đến đây, chúng ta đã đề cập đến việc hợp nhất thông thường của hai nhánh, thường được xử lý bằng một chiến lược hợp nhất gọi là "`đệ quy`". Tuy nhiên, có những cách khác để hợp nhất các nhánh lại với nhau. Hãy cùng xem qua một vài trong số đó một cách nhanh chóng. ===== Ưu tiên Của chúng ta hoặc Của họ Trước hết, có một điều hữu ích khác chúng ta có thể làm với chế độ hợp nhất "`đệ quy`" thông thường. Chúng ta đã thấy các tùy chọn `ignore-all-space` và `ignore-space-change` được truyền bằng một `-X` nhưng chúng ta cũng có thể bảo Git ưu tiên một bên hoặc bên kia khi nó thấy một xung đột. Theo mặc định, khi Git thấy một xung đột giữa hai nhánh đang được hợp nhất, nó sẽ thêm các dấu xung đột hợp nhất vào mã của bạn và đánh dấu tệp là bị xung đột và để bạn giải quyết nó. Nếu bạn muốn Git chỉ cần chọn một bên cụ thể và bỏ qua bên kia thay vì để bạn giải quyết xung đột thủ công, bạn có thể truyền cho lệnh `merge` hoặc `-Xours` hoặc `-Xtheirs`. Nếu Git thấy điều này, nó sẽ không thêm các dấu xung đột. Bất kỳ sự khác biệt nào có thể hợp nhất, nó sẽ hợp nhất. Bất kỳ sự khác biệt nào xung đột, nó sẽ chỉ chọn bên bạn chỉ định toàn bộ, bao gồm cả các tệp nhị phân. Nếu chúng ta quay lại ví dụ "`hello world`" mà chúng ta đã sử dụng trước đó, chúng ta có thể thấy rằng việc hợp nhất nhánh của chúng ta gây ra xung đột. [source,console]
$ git merge mundo Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Resolved 'hello.rb' using previous resolution. Automatic merge failed; fix conflicts and then commit the result.
Tuy nhiên, nếu chúng ta chạy nó với `-Xours` hoặc `-Xtheirs` thì không. [source,console]
$ git merge -Xours mundo Auto-merging hello.rb Merge made by the 'recursive' strategy. hello.rb | 2 - test.sh | 2 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 test.sh
Trong trường hợp đó, thay vì nhận được các dấu xung đột trong tệp với "`hello mundo`" ở một bên và "`hola world`" ở bên kia, nó sẽ chỉ chọn "`hola world`". Tuy nhiên, tất cả các thay đổi không xung đột khác trên nhánh đó đều được hợp nhất thành công. Tùy chọn này cũng có thể được truyền cho lệnh `git merge-file` mà chúng ta đã thấy trước đó bằng cách chạy một cái gì đó như `git merge-file --ours` cho các lần hợp nhất tệp riêng lẻ. Nếu bạn muốn làm một cái gì đó như thế này nhưng không muốn Git thậm chí cố gắng hợp nhất các thay đổi từ phía bên kia, có một tùy chọn hà khắc hơn, đó là _chiến lược_ hợp nhất "`ours`". Điều này khác với _tùy chọn_ hợp nhất đệ quy "`ours`". Về cơ bản, điều này sẽ thực hiện một hợp nhất giả. Nó sẽ ghi lại một cam kết hợp nhất mới với cả hai nhánh là cha mẹ, nhưng nó sẽ không nhìn vào nhánh bạn đang hợp nhất. Nó sẽ chỉ ghi lại kết quả của việc hợp nhất là mã chính xác trong nhánh hiện tại của bạn. [source,console]
$ git merge -s ours mundo Merge made by the 'ours' strategy. $ git diff HEAD HEAD~ $
Bạn có thể thấy rằng không có sự khác biệt giữa nhánh chúng ta đang ở và kết quả của việc hợp nhất. Điều này thường có thể hữu ích để về cơ bản lừa Git nghĩ rằng một nhánh đã được hợp nhất khi thực hiện một hợp nhất sau này. Ví dụ, giả sử bạn đã phân nhánh từ một nhánh `release` và đã thực hiện một số công việc trên đó mà bạn sẽ muốn hợp nhất trở lại vào nhánh `master` của mình vào một thời điểm nào đó. Trong khi đó, một số bản sửa lỗi trên `master` cần được chuyển ngược vào nhánh `release` của bạn. Bạn có thể hợp nhất nhánh sửa lỗi vào nhánh `release` và cũng `merge -s ours` cùng một nhánh vào nhánh `master` của bạn (mặc dù bản sửa lỗi đã có ở đó) để khi bạn hợp nhất lại nhánh `release` sau này, không có xung đột nào từ bản sửa lỗi. [[_subtree_merge]] ===== Hợp nhất Cây con (Subtree Merging) Ý tưởng của việc hợp nhất cây con là bạn có hai dự án, và một trong các dự án ánh xạ tới một thư mục con của dự án kia. Khi bạn chỉ định một hợp nhất cây con, Git thường đủ thông minh để nhận ra rằng một cái là cây con của cái kia và hợp nhất một cách thích hợp. Chúng ta sẽ đi qua một ví dụ về việc thêm một dự án riêng biệt vào một dự án hiện có và sau đó hợp nhất mã của dự án thứ hai vào một thư mục con của dự án thứ nhất. Đầu tiên, chúng ta sẽ thêm ứng dụng Rack vào dự án của mình. Chúng ta sẽ thêm dự án Rack dưới dạng một tham chiếu từ xa trong dự án của riêng mình và sau đó checkout nó vào nhánh riêng của nó: [source,console]
$ git remote add rack_remote https://github.com/rack/rack $ git fetch rack_remote --no-tags warning: no common commits remote: Counting objects: 3184, done. remote: Compressing objects: 100% (1465/1465), done. remote: Total 3184 (delta 1952), reused 2770 (delta 1675) Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done. Resolving deltas: 100% (1952/1952), done. From https://github.com/rack/rack * [new branch] build → rack_remote/build * [new branch] master → rack_remote/master * [new branch] rack-0.4 → rack_remote/rack-0.4 * [new branch] rack-0.9 → rack_remote/rack-0.9 $ git checkout -b rack_branch rack_remote/master Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master. Switched to a new branch "rack_branch"
Bây giờ chúng ta có gốc của dự án Rack trong nhánh `rack_branch` của chúng ta và dự án riêng của chúng ta trong nhánh `master`. Nếu bạn checkout cái này rồi cái kia, bạn có thể thấy rằng chúng có các gốc dự án khác nhau: [source,console]
$ ls AUTHORS KNOWN-ISSUES Rakefile contrib lib COPYING README bin example test $ git checkout master Switched to branch "master" $ ls README
Đây là một khái niệm hơi lạ. Không phải tất cả các nhánh trong kho lưu trữ của bạn thực sự phải là các nhánh của cùng một dự án. Điều này không phổ biến, bởi vì nó hiếm khi hữu ích, nhưng khá dễ dàng để có các nhánh chứa các lịch sử hoàn toàn khác nhau. Trong trường hợp này, chúng ta muốn kéo dự án Rack vào dự án `master` của chúng ta dưới dạng một thư mục con. Chúng ta có thể làm điều đó trong Git với `git read-tree`. Bạn sẽ tìm hiểu thêm về `read-tree` và những người bạn của nó trong <<ch10-git-internals#ch10-git-internals>>, nhưng hiện tại hãy biết rằng nó đọc cây gốc của một nhánh vào khu vực tổ chức và thư mục làm việc hiện tại của bạn. Chúng ta vừa chuyển trở lại nhánh `master` của bạn, và chúng ta kéo nhánh `rack_branch` vào thư mục con `rack` của nhánh `master` của dự án chính của chúng ta: [source,console]
$ git read-tree --prefix=rack/ -u rack_branch
Khi chúng ta commit, có vẻ như chúng ta có tất cả các tệp Rack dưới thư mục con đó -- như thể chúng ta đã sao chép chúng vào từ một tarball. Điều thú vị là chúng ta có thể khá dễ dàng hợp nhất các thay đổi từ một trong các nhánh sang nhánh kia. Vì vậy, nếu dự án Rack cập nhật, chúng ta có thể kéo vào các thay đổi thượng nguồn bằng cách chuyển sang nhánh đó và kéo (pulling): [source,console]
$ git checkout rack_branch $ git pull
Sau đó, chúng ta có thể hợp nhất các thay đổi đó trở lại vào nhánh `master` của mình. Để kéo vào các thay đổi và điền trước thông điệp commit, hãy sử dụng tùy chọn `--squash`, cũng như tùy chọn `-Xsubtree` của chiến lược hợp nhất đệ quy. Chiến lược đệ quy là mặc định ở đây, nhưng chúng ta bao gồm nó cho rõ ràng. [source,console]
$ git checkout master $ git merge --squash -s recursive -Xsubtree=rack rack_branch Squash commit — not updating HEAD Automatic merge went well; stopped before committing as requested
Tất cả các thay đổi từ dự án Rack được hợp nhất vào và sẵn sàng để được commit cục bộ. Bạn cũng có thể làm ngược lại -- thực hiện các thay đổi trong thư mục con `rack` của nhánh `master` của bạn và sau đó hợp nhất chúng vào nhánh `rack_branch` của bạn sau này để gửi chúng cho những người bảo trì hoặc đẩy chúng lên thượng nguồn. Điều này cung cấp cho chúng ta một cách để có một quy trình làm việc hơi giống với quy trình làm việc mô-đun con (submodule) mà không cần sử dụng các mô-đun con (mà chúng ta sẽ đề cập trong <<ch07-git-tools#_git_submodules>>). Chúng ta có thể giữ các nhánh với các dự án liên quan khác trong kho lưu trữ của mình và hợp nhất cây con chúng vào dự án của mình thỉnh thoảng. Nó tốt theo một số cách, ví dụ tất cả mã được commit vào một nơi duy nhất. Tuy nhiên, nó có những hạn chế khác ở chỗ nó phức tạp hơn một chút và dễ mắc lỗi hơn trong việc tích hợp lại các thay đổi hoặc vô tình đẩy một nhánh vào một kho lưu trữ không liên quan. Một điều hơi kỳ lạ nữa là để lấy diff giữa những gì bạn có trong thư mục con `rack` của bạn và mã trong nhánh `rack_branch` của bạn -- để xem liệu bạn có cần hợp nhất chúng hay không -- bạn không thể sử dụng lệnh `diff` thông thường. Thay vào đó, bạn phải chạy `git diff-tree` với nhánh bạn muốn so sánh với: [source,console]
$ git diff-tree -p rack_branch
Hoặc, để so sánh những gì có trong thư mục con `rack` của bạn với những gì nhánh `master` trên máy chủ là lần cuối cùng bạn fetch, bạn có thể chạy: [source,console]
$ git diff-tree -p rack_remote/master
[[ref_rerere]] === Rerere Chức năng `git rerere` là một tính năng hơi ẩn. Tên này là viết tắt của "`reuse recorded resolution`" (tái sử dụng giải pháp đã ghi lại) và, như tên gọi, nó cho phép bạn yêu cầu Git ghi nhớ cách bạn đã giải quyết một xung đột đoạn mã để lần sau khi nó thấy xung đột tương tự, Git có thể tự động giải quyết nó cho bạn. Có một số kịch bản mà chức năng này có thể thực sự tiện dụng. Một trong những ví dụ được đề cập trong tài liệu là khi bạn muốn đảm bảo một nhánh chủ đề có tuổi thọ cao cuối cùng sẽ hợp nhất sạch sẽ, nhưng bạn không muốn có nhiều cam kết hợp nhất trung gian làm lộn xộn lịch sử cam kết của bạn. Với `rerere` được bật, bạn có thể thử hợp nhất không thường xuyên, giải quyết xung đột, sau đó rút lại việc hợp nhất. Nếu bạn làm điều này liên tục, thì lần hợp nhất cuối cùng sẽ dễ dàng vì `rerere` có thể tự động làm mọi thứ cho bạn. Chiến thuật tương tự có thể được sử dụng nếu bạn muốn giữ một nhánh được rebase để bạn không phải đối phó với cùng một xung đột rebase mỗi khi bạn thực hiện nó. Hoặc nếu bạn muốn lấy một nhánh bạn đã hợp nhất và sửa một loạt xung đột và sau đó quyết định rebase nó -- bạn có thể sẽ không phải làm lại tất cả các xung đột tương tự. Một ứng dụng khác của `rerere` là khi bạn hợp nhất một loạt các nhánh chủ đề đang phát triển lại với nhau thành một đầu có thể kiểm thử không thường xuyên, như dự án Git thường làm. Nếu các thử nghiệm thất bại, bạn có thể hoàn tác các lần hợp nhất và thực hiện lại chúng mà không có nhánh chủ đề đã làm cho các thử nghiệm thất bại mà không cần phải giải quyết lại các xung đột. Để bật chức năng `rerere`, bạn chỉ cần chạy cài đặt cấu hình này: [source,console]
$ git config --global rerere.enabled true
Bạn cũng có thể bật nó bằng cách tạo thư mục `.git/rr-cache` trong một kho lưu trữ cụ thể, nhưng cài đặt cấu hình rõ ràng hơn và bật tính năng đó trên toàn cầu cho bạn. Bây giờ hãy xem một ví dụ đơn giản, tương tự như ví dụ trước của chúng ta. Giả sử chúng ta có một tệp có tên `hello.rb` trông như thế này: [source,ruby]
#! /usr/bin/env ruby
def hello puts 'hello world' end
Trong một nhánh, chúng ta thay đổi từ "`hello`" thành "`hola`", sau đó trong một nhánh khác, chúng ta thay đổi "`world`" thành "`mundo`", giống như trước. .Hai nhánh thay đổi cùng một phần của cùng một tệp khác nhau image::images/rerere1.png[Hai nhánh thay đổi cùng một phần của cùng một tệp khác nhau] Khi chúng ta hợp nhất hai nhánh lại với nhau, chúng ta sẽ gặp xung đột hợp nhất: [source,console]
$ git merge i18n-world Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Recorded preimage for 'hello.rb' Automatic merge failed; fix conflicts and then commit the result.
Bạn nên lưu ý dòng mới `Recorded preimage for FILE` ở đó. Nếu không thì nó sẽ trông giống hệt như một xung đột hợp nhất thông thường. Tại thời điểm này, `rerere` có thể cho chúng ta biết một vài điều. Thông thường, bạn có thể chạy `git status` tại thời điểm này để xem tất cả những gì bị xung đột: [source,console]
$ git status # On branch master # Unmerged paths: # (use "git reset HEAD <file>…" to unstage) # (use "git add <file>…" to mark resolution) # # both modified: hello.rb #
Tuy nhiên, `git rerere` cũng sẽ cho bạn biết nó đã ghi lại trạng thái trước khi hợp nhất cho những gì bằng `git rerere status`: [source,console]
$ git rerere status hello.rb
Và `git rerere diff` sẽ hiển thị trạng thái hiện tại của giải pháp -- những gì bạn đã bắt đầu để giải quyết và những gì bạn đã giải quyết nó thành. [source,console]
$ git rerere diff --- a/hello.rb + b/hello.rb @@ -1,11 +1,11 @@ #! /usr/bin/env ruby
def hello -<<<<<<< - puts 'hello mundo' -======= +<<<<<<< HEAD puts 'hola world' ->>>>>>> +======= + puts 'hello mundo' +>>>>>>> i18n-world end
Cũng (và điều này không thực sự liên quan đến `rerere`), bạn có thể sử dụng `git ls-files -u` để xem các tệp xung đột và các phiên bản trước, trái và phải: [source,console]
$ git ls-files -u 100644 39804c942a9c1f2c03dc7c5ebcd7f3e3a6b97519 1 hello.rb 100644 a440db6e8d1fd76ad438a49025a9ad9ce746f581 2 hello.rb 100644 54336ba847c3758ab604876419607e9443848474 3 hello.rb
Bây giờ bạn có thể giải quyết nó để chỉ còn `puts 'hola mundo'` và bạn có thể chạy `git rerere diff` một lần nữa để xem rerere sẽ ghi nhớ những gì: [source,console]
$ git rerere diff --- a/hello.rb + b/hello.rb @@ -1,11 +1,7 @@ #! /usr/bin/env ruby
def hello -<<<<<<< - puts 'hello mundo' -======= - puts 'hola world' ->>>>>>> + puts 'hola mundo' end
Vì vậy, về cơ bản nó nói rằng, khi Git thấy một xung đột đoạn mã trong một tệp `hello.rb` có "`hello mundo`" ở một bên và "`hola world`" ở bên kia, nó sẽ giải quyết nó thành "`hola mundo`". Bây giờ chúng ta có thể đánh dấu nó là đã giải quyết và cam kết nó: [source,console]
$ git add hello.rb $ git commit Recorded resolution for 'hello.rb'. [master 68e16e5] Merge branch 'i18n'
Bạn có thể thấy rằng nó "Recorded resolution for FILE". .Giải pháp đã ghi lại cho FILE image::images/rerere2.png[Giải pháp đã ghi lại cho FILE] Bây giờ, hãy hoàn tác việc hợp nhất đó và sau đó rebase nó trên đỉnh nhánh `master` của chúng ta. Chúng ta có thể di chuyển nhánh của mình trở lại bằng cách sử dụng `git reset` như chúng ta đã thấy trong <<ch07-git-tools#_git_reset>>. [source,console]
$ git reset --hard HEAD^ HEAD is now at ad63f15 i18n the hello
Việc hợp nhất của chúng ta đã được hoàn tác. Bây giờ hãy rebase nhánh chủ đề. [source,console]
$ git checkout i18n-world Switched to branch 'i18n-world'
$ git rebase master First, rewinding head to replay your work on top of it… Applying: i18n one word Using index info to reconstruct a base tree… Falling back to patching base and 3-way merge… Auto-merging hello.rb CONFLICT (content): Merge conflict in hello.rb Resolved 'hello.rb' using previous resolution. Failed to merge in the changes. Patch failed at 0001 i18n one word
Bây giờ, chúng ta đã gặp cùng một xung đột hợp nhất như chúng ta mong đợi, nhưng hãy xem dòng `Resolved FILE using previous resolution`. Nếu chúng ta nhìn vào tệp, chúng ta sẽ thấy rằng nó đã được giải quyết, không có dấu xung đột hợp nhất nào trong đó. [source,ruby]
#! /usr/bin/env ruby
def hello puts 'hola mundo' end
Ngoài ra, `git diff` sẽ hiển thị cho bạn cách nó đã được giải quyết lại tự động: [source,console]
$ git diff diff --cc hello.rb index a440db6,54336ba..0000000 --- a/hello.rb + b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby
def hello - puts 'hola world' - puts 'hello mundo' ++ puts 'hola mundo' end
.Xung đột hợp nhất được giải quyết tự động bằng giải pháp trước đó image::images/rerere3.png[Xung đột hợp nhất được giải quyết tự động bằng giải pháp trước đó] Bạn cũng có thể tạo lại trạng thái tệp bị xung đột bằng `git checkout`: [source,console]
$ git checkout --conflict=merge hello.rb $ cat hello.rb #! /usr/bin/env ruby
def hello <<<<<<< ours puts 'hola world'
puts 'hello mundo' >>>>>>> theirs end
Chúng ta đã thấy một ví dụ về điều này trong <<ch07-git-tools#_advanced_merging>>. Tuy nhiên, bây giờ, hãy giải quyết lại nó bằng cách chạy `git rerere` một lần nữa: [source,console]
$ git rerere Resolved 'hello.rb' using previous resolution. $ cat hello.rb #! /usr/bin/env ruby
def hello puts 'hola mundo' end
Chúng ta đã giải quyết lại tệp tự động bằng cách sử dụng giải pháp `rerere` đã được lưu vào bộ nhớ cache. Bây giờ bạn có thể thêm và tiếp tục rebase để hoàn thành nó. [source,console]
$ git add hello.rb $ git rebase --continue Applying: i18n one word
Vì vậy, nếu bạn thực hiện nhiều lần hợp nhất lại, hoặc muốn giữ một nhánh chủ đề được cập nhật với nhánh `master` của bạn mà không cần nhiều lần hợp nhất, hoặc bạn rebase thường xuyên, bạn có thể bật `rerere` để giúp cuộc sống của bạn dễ dàng hơn một chút. === Gỡ lỗi với Git Ngoài việc chủ yếu dùng để kiểm soát phiên bản, Git còn cung cấp một vài lệnh để giúp bạn gỡ lỗi các dự án mã nguồn của mình. Vì Git được thiết kế để xử lý hầu hết mọi loại nội dung, các công cụ này khá chung chung, nhưng chúng thường có thể giúp bạn tìm kiếm lỗi hoặc thủ phạm khi mọi thứ không ổn. [[_file_annotation]] ==== Chú thích Tệp Nếu bạn tìm thấy một lỗi trong mã của mình và muốn biết nó được giới thiệu khi nào và tại sao, chú thích tệp thường là công cụ tốt nhất của bạn. Nó cho bạn biết cam kết nào là cam kết cuối cùng đã sửa đổi mỗi dòng của bất kỳ tệp nào. Vì vậy, nếu bạn thấy một phương thức trong mã của mình bị lỗi, bạn có thể chú thích tệp bằng `git blame` để xác định cam kết nào chịu trách nhiệm cho việc giới thiệu dòng đó. Ví dụ sau sử dụng `git blame` để xác định cam kết và người cam kết nào chịu trách nhiệm cho các dòng trong `Makefile` cấp cao nhất của nhân Linux và, xa hơn, sử dụng tùy chọn `-L` để hạn chế đầu ra của chú thích đến các dòng 69 đến 82 của tệp đó: [source,console]
$ git blame -L 69,82 Makefile b8b0618cf6fab (Cheng Renquan 2009-05-26 16:03:07 +0800 69) ifeq ("$(origin V)", "command line") b8b0618cf6fab (Cheng Renquan 2009-05-26 16:03:07 +0800 70) KBUILD_VERBOSE = $(V) ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 71) endif ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 72) ifndef KBUILD_VERBOSE ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 73) KBUILD_VERBOSE = 0 ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 74) endif ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 75) 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 76) ifeq ($(KBUILD_VERBOSE),1) 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 77) quiet = 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 78) Q = 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 79) else 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 80) quiet=quiet_ 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 81) Q = @ 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 82) endif
Lưu ý rằng trường đầu tiên là SHA-1 một phần của cam kết cuối cùng đã sửa đổi dòng đó. Hai trường tiếp theo là các giá trị được trích xuất từ cam kết đó -- tên tác giả và ngày tạo của cam kết đó -- vì vậy bạn có thể dễ dàng thấy ai đã sửa đổi dòng đó và khi nào. Sau đó là số dòng và nội dung của tệp. Cũng lưu ý các dòng cam kết `^1da177e4c3f4`, trong đó tiền tố `^` chỉ định các dòng được giới thiệu trong cam kết ban đầu của kho lưu trữ và vẫn không thay đổi kể từ đó. Điều này hơi khó hiểu, bởi vì bây giờ bạn đã thấy ít nhất ba cách khác nhau mà Git sử dụng `^` để sửa đổi SHA-1 cam kết, nhưng đó là ý nghĩa của nó ở đây. Một điều thú vị khác về Git là nó không theo dõi rõ ràng việc đổi tên tệp. Nó ghi lại các ảnh chụp nhanh và sau đó cố gắng tìm ra những gì đã được đổi tên một cách ngầm định, sau đó. Một trong những tính năng thú vị của điều này là bạn có thể yêu cầu nó tìm ra tất cả các loại chuyển động mã. Nếu bạn truyền `-C` cho `git blame`, Git phân tích tệp bạn đang chú thích và cố gắng tìm ra các đoạn mã trong đó ban đầu đến từ đâu nếu chúng được sao chép từ nơi khác. Ví dụ, giả sử bạn đang tái cấu trúc một tệp có tên `GITServerHandler.m` thành nhiều tệp, một trong số đó là `GITPackUpload.m`. Bằng cách blame `GITPackUpload.m` với tùy chọn `-C`, bạn có thể thấy các phần của mã ban đầu đến từ đâu: [source,console]
$ git blame -C -L 141,153 GITPackUpload.m f344f58d GITServerHandler.m (Scott 2009-01-04 141) f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC f344f58d GITServerHandler.m (Scott 2009-01-04 143) { 70befddd GITServerHandler.m (Scott 2009-03-22 144) //NSLog(@"GATHER COMMI ad11ac80 GITPackUpload.m (Scott 2009-03-24 145) ad11ac80 GITPackUpload.m (Scott 2009-03-24 146) NSString *parentSha; ad11ac80 GITPackUpload.m (Scott 2009-03-24 147) GITCommit *commit = [g ad11ac80 GITPackUpload.m (Scott 2009-03-24 148) ad11ac80 GITPackUpload.m (Scott 2009-03-24 149) //NSLog(@"GATHER COMMI ad11ac80 GITPackUpload.m (Scott 2009-03-24 150) 56ef2caf GITServerHandler.m (Scott 2009-01-05 151) if(commit) { 56ef2caf GITServerHandler.m (Scott 2009-01-05 152) [refDict setOb 56ef2caf GITServerHandler.m (Scott 2009-01-05 153)
Điều này thực sự hữu ích. Thông thường, bạn sẽ nhận được cam kết ban đầu là cam kết nơi bạn sao chép mã, bởi vì đó là lần đầu tiên bạn chạm vào các dòng đó trong tệp này. Git cho bạn biết cam kết ban đầu nơi bạn đã viết các dòng đó, ngay cả khi nó nằm trong một tệp khác. [[_binary_search]] ==== Tìm kiếm Nhị phân Chú thích tệp hữu ích nếu bạn biết vấn đề ở đâu ngay từ đầu. Nếu bạn không biết điều gì đang gây ra lỗi, và đã có hàng chục hoặc hàng trăm cam kết kể từ trạng thái cuối cùng mà bạn biết mã hoạt động, bạn có thể sẽ tìm đến `git bisect` để được trợ giúp. Lệnh `bisect` thực hiện tìm kiếm nhị phân trong lịch sử cam kết của bạn để giúp bạn xác định càng nhanh càng tốt cam kết nào đã giới thiệu một vấn đề. Giả sử bạn vừa đẩy một bản phát hành mã của mình lên môi trường sản xuất, bạn đang nhận được báo cáo lỗi về điều gì đó không xảy ra trong môi trường phát triển của bạn, và bạn không thể tưởng tượng tại sao mã lại làm điều đó. Bạn quay lại mã của mình, và hóa ra bạn có thể tái hiện vấn đề, nhưng bạn không thể tìm ra điều gì đang xảy ra. Bạn có thể _chia đôi_ mã để tìm hiểu. Đầu tiên bạn chạy `git bisect start` để bắt đầu, và sau đó bạn sử dụng `git bisect bad` để thông báo cho hệ thống rằng cam kết hiện tại bạn đang ở bị lỗi. Sau đó, bạn phải cho bisect biết trạng thái tốt cuối cùng được biết là khi nào, sử dụng `git bisect good <good_commit>`: [source,console]
$ git bisect start $ git bisect bad $ git bisect good v1.0 Bisecting: 6 revisions left to test after this [ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] Error handling on repo
Git đã tìm ra rằng khoảng 12 cam kết nằm giữa cam kết bạn đã đánh dấu là cam kết tốt cuối cùng (v1.0) và phiên bản lỗi hiện tại, và nó đã kiểm tra cam kết ở giữa cho bạn. Tại thời điểm này, bạn có thể chạy thử nghiệm của mình để xem liệu vấn đề có tồn tại tại cam kết này hay không. Nếu có, thì nó đã được giới thiệu vào một thời điểm nào đó trước cam kết ở giữa này; nếu không, thì vấn đề đã được giới thiệu vào một thời điểm nào đó sau cam kết ở giữa này. Hóa ra không có vấn đề gì ở đây, và bạn thông báo cho Git điều đó bằng cách gõ `git bisect good` và tiếp tục hành trình của bạn: [source,console]
$ git bisect good Bisecting: 3 revisions left to test after this [b047b02ea83310a70fd603dc8cd7a6cd13d15c04] Secure this thing
Bây giờ bạn đang ở trên một cam kết khác, nằm giữa cam kết bạn vừa kiểm tra và cam kết lỗi của bạn. Bạn chạy thử nghiệm của mình một lần nữa và thấy rằng cam kết này bị lỗi, vì vậy bạn thông báo cho Git điều đó bằng `git bisect bad`: [source,console]
$ git bisect bad Bisecting: 1 revisions left to test after this [f71ce38690acf49c1f3c9bea38e09d82a5ce6014] Drop exceptions table
Cam kết này tốt, và bây giờ Git có tất cả thông tin cần thiết để xác định vấn đề đã được giới thiệu ở đâu. Nó cho bạn biết SHA-1 của cam kết lỗi đầu tiên và hiển thị một số thông tin cam kết và những tệp nào đã được sửa đổi trong cam kết đó để bạn có thể tìm ra điều gì đã xảy ra có thể đã giới thiệu lỗi này: [source,console]
$ git bisect good b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04 Author: PJ Hyett <pjhyett@example.com> Date: Tue Jan 27 14:48:32 2009 -0800
Secure this thing
:040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730 f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M config
Khi bạn hoàn tất, bạn nên chạy `git bisect reset` để đặt lại HEAD của bạn về nơi bạn đã ở trước khi bạn bắt đầu, nếu không bạn sẽ ở trạng thái kỳ lạ: [source,console]
$ git bisect reset
Đây là một công cụ mạnh mẽ có thể giúp bạn kiểm tra hàng trăm cam kết để tìm một lỗi được giới thiệu trong vài phút. Trên thực tế, nếu bạn có một tập lệnh sẽ thoát 0 nếu dự án tốt hoặc khác 0 nếu dự án xấu, bạn có thể tự động hóa hoàn toàn `git bisect`. Đầu tiên, bạn lại cho nó biết phạm vi của bisect bằng cách cung cấp các cam kết xấu và tốt đã biết. Bạn có thể làm điều này bằng cách liệt kê chúng với lệnh `bisect start` nếu bạn muốn, liệt kê cam kết xấu đã biết trước và cam kết tốt đã biết thứ hai: [source,console]
$ git bisect start HEAD v1.0 $ git bisect run test-error.sh
Làm như vậy tự động chạy `test-error.sh` trên mỗi cam kết đã kiểm tra cho đến khi Git tìm thấy cam kết bị lỗi đầu tiên. Bạn cũng có thể chạy một cái gì đó như `make` hoặc `make tests` hoặc bất cứ thứ gì bạn có để chạy các bài kiểm tra tự động cho bạn. [[_git_submodules]] === Mô-đun con (Submodules) Thường xảy ra trường hợp khi đang làm việc trên một dự án, bạn cần sử dụng một dự án khác từ bên trong nó. Có lẽ đó là một thư viện mà bên thứ ba đã phát triển hoặc bạn đang phát triển riêng biệt và sử dụng trong nhiều dự án cha. Một vấn đề phổ biến nảy sinh trong các tình huống này: bạn muốn có thể coi hai dự án là riêng biệt nhưng vẫn có thể sử dụng một dự án từ bên trong dự án kia. Dưới đây là một ví dụ. Giả sử bạn đang phát triển một trang web và tạo nguồn cấp dữ liệu Atom. Thay vì viết mã tạo Atom của riêng bạn, bạn quyết định sử dụng một thư viện. Bạn có thể sẽ phải bao gồm mã này từ một thư viện chia sẻ như cài đặt CPAN hoặc Ruby gem, hoặc sao chép mã nguồn vào cây dự án của riêng bạn. Vấn đề với việc bao gồm thư viện là khó tùy chỉnh thư viện theo bất kỳ cách nào và thường khó triển khai hơn, bởi vì bạn cần đảm bảo mọi máy khách đều có sẵn thư viện đó. Vấn đề với việc sao chép mã vào dự án của riêng bạn là bất kỳ thay đổi tùy chỉnh nào bạn thực hiện đều khó hợp nhất khi các thay đổi thượng nguồn trở nên khả dụng. Git giải quyết vấn đề này bằng cách sử dụng các mô-đun con. Các mô-đun con cho phép bạn giữ một kho lưu trữ Git dưới dạng một thư mục con của một kho lưu trữ Git khác. Điều này cho phép bạn sao chép một kho lưu trữ khác vào dự án của mình và giữ các cam kết của bạn tách biệt. [[_starting_submodules]] ==== Bắt đầu với các Mô-đun con Chúng ta sẽ đi qua việc phát triển một dự án đơn giản đã được chia thành một dự án chính và một vài dự án con. Hãy bắt đầu bằng cách thêm một kho lưu trữ Git hiện có làm mô-đun con của kho lưu trữ mà chúng ta đang làm việc. Để thêm một mô-đun con mới, bạn sử dụng lệnh `git submodule add` với URL tuyệt đối hoặc tương đối của dự án bạn muốn bắt đầu theo dõi. Trong ví dụ này, chúng ta sẽ thêm một thư viện có tên "`DbConnector`". [source,console]
$ git submodule add https://github.com/chaconinc/DbConnector Cloning into 'DbConnector'… remote: Counting objects: 11, done. remote: Compressing objects: 100% (10/10), done. remote: Total 11 (delta 0), reused 11 (delta 0) Unpacking objects: 100% (11/11), done. Checking connectivity… done.
Theo mặc định, các mô-đun con sẽ thêm dự án con vào một thư mục có tên giống với kho lưu trữ, trong trường hợp này là "`DbConnector`". Bạn có thể thêm một đường dẫn khác vào cuối lệnh nếu bạn muốn nó đi đến nơi khác. Nếu bạn chạy `git status` tại thời điểm này, bạn sẽ nhận thấy một vài điều. [source,console]
$ git status On branch master Your branch is up-to-date with 'origin/master'.
Changes to be committed: (use "git reset HEAD <file>…" to unstage)
new file: .gitmodules new file: DbConnector
Đầu tiên bạn nên chú ý đến tệp `.gitmodules` mới. Đây là một tệp cấu hình lưu trữ ánh xạ giữa URL của dự án và thư mục con cục bộ mà bạn đã kéo nó vào: [source,ini]
path = DbConnector url = https://github.com/chaconinc/DbConnector
Nếu bạn có nhiều mô-đun con, bạn sẽ có nhiều mục trong tệp này. Điều quan trọng cần lưu ý là tệp này được kiểm soát phiên bản cùng với các tệp khác của bạn, giống như tệp `.gitignore` của bạn. Nó được đẩy và kéo cùng với phần còn lại của dự án của bạn. Đây là cách những người khác sao chép dự án này biết nơi lấy các dự án mô-đun con. [NOTE] ===== Vì URL trong tệp .gitmodules là những gì người khác sẽ thử sao chép/fetch đầu tiên, hãy đảm bảo sử dụng URL mà họ có thể truy cập nếu có thể. Ví dụ, nếu bạn sử dụng một URL khác để đẩy so với những người khác sẽ sử dụng để kéo, hãy sử dụng URL mà những người khác có quyền truy cập. Bạn có thể ghi đè giá trị này cục bộ bằng `git config submodule.DbConnector.url PRIVATE_URL` để sử dụng cho riêng bạn. Khi áp dụng, một URL tương đối có thể hữu ích. ===== Mục liệt kê khác trong đầu ra `git status` là mục nhập thư mục dự án. Nếu bạn chạy `git diff` trên đó, bạn sẽ thấy điều gì đó thú vị: [source,console]
$ git diff --cached DbConnector diff --git a/DbConnector b/DbConnector new file mode 160000 index 0000000..c3f01dc --- /dev/null + b/DbConnector @@ -0,0 +1 @@ +Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc
Mặc dù `DbConnector` là một thư mục con trong thư mục làm việc của bạn, Git coi nó là một mô-đun con và không theo dõi nội dung của nó khi bạn không ở trong thư mục đó. Thay vào đó, Git coi nó là một cam kết cụ thể từ kho lưu trữ đó. Nếu bạn muốn đầu ra diff đẹp hơn một chút, bạn có thể chuyển tùy chọn `--submodule` cho `git diff`. [source,console]
$ git diff --cached --submodule diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..71fc376 --- /dev/null + b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "DbConnector"] + path = DbConnector + url = https://github.com/chaconinc/DbConnector Submodule DbConnector 0000000…c3f01dc (new submodule)
Khi bạn cam kết, bạn thấy một cái gì đó giống như thế này: [source,console]
$ git commit -am 'Add DbConnector module' [master fb9093c] Add DbConnector module 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 DbConnector
Lưu ý chế độ `160000` cho mục nhập `DbConnector`. Đó là một chế độ đặc biệt trong Git về cơ bản có nghĩa là bạn đang ghi lại một cam kết dưới dạng một mục nhập thư mục thay vì một thư mục con hoặc một tệp. Cuối cùng, đẩy các thay đổi này: [source,console]
$ git push origin master
[[_cloning_submodules]] ==== Sao chép một Dự án với các Mô-đun con Ở đây chúng ta sẽ sao chép một dự án có một mô-đun con trong đó. Khi bạn sao chép một dự án như vậy, theo mặc định bạn nhận được các thư mục chứa các mô-đun con, nhưng chưa có tệp nào bên trong chúng: [source,console]
$ git clone https://github.com/chaconinc/MainProject Cloning into 'MainProject'… remote: Counting objects: 14, done. remote: Compressing objects: 100% (13/13), done. remote: Total 14 (delta 1), reused 13 (delta 0) Unpacking objects: 100% (14/14), done. Checking connectivity… done. $ cd MainProject $ ls -la total 16 drwxr-xr-x 9 schacon staff 306 Sep 17 15:21 . drwxr-xr-x 7 schacon staff 238 Sep 17 15:21 .. drwxr-xr-x 13 schacon staff 442 Sep 17 15:21 .git -rw-r—r-- 1 schacon staff 92 Sep 17 15:21 .gitmodules drwxr-xr-x 2 schacon staff 68 Sep 17 15:21 DbConnector -rw-r—r-- 1 schacon staff 756 Sep 17 15:21 Makefile drwxr-xr-x 3 schacon staff 102 Sep 17 15:21 includes drwxr-xr-x 4 schacon staff 136 Sep 17 15:21 scripts drwxr-xr-x 4 schacon staff 136 Sep 17 15:21 src $ cd DbConnector/ $ ls $
Thư mục `DbConnector` có ở đó, nhưng trống rỗng. Bạn phải chạy hai lệnh từ dự án chính: `git submodule init` để khởi tạo tệp cấu hình cục bộ của bạn, và `git submodule update` để lấy tất cả dữ liệu từ dự án đó và checkout cam kết thích hợp được liệt kê trong siêu dự án của bạn: [source,console]
$ git submodule init Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector' $ git submodule update Cloning into 'DbConnector'… remote: Counting objects: 11, done. remote: Compressing objects: 100% (10/10), done. remote: Total 11 (delta 0), reused 11 (delta 0) Unpacking objects: 100% (11/11), done. Checking connectivity… done. Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'
Bây giờ thư mục con `DbConnector` của bạn đang ở trạng thái chính xác như khi bạn cam kết trước đó. Tuy nhiên, có một cách khác để làm điều này đơn giản hơn một chút. Nếu bạn chuyển `--recurse-submodules` cho lệnh `git clone`, nó sẽ tự động khởi tạo và cập nhật từng mô-đun con trong kho lưu trữ, bao gồm cả các mô-đun con lồng nhau nếu bất kỳ mô-đun con nào trong kho lưu trữ cũng có các mô-đun con. [source,console]
$ git clone --recurse-submodules https://github.com/chaconinc/MainProject Cloning into 'MainProject'… remote: Counting objects: 14, done. remote: Compressing objects: 100% (13/13), done. remote: Total 14 (delta 1), reused 13 (delta 0) Unpacking objects: 100% (14/14), done. Checking connectivity… done. Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector' Cloning into 'DbConnector'… remote: Counting objects: 11, done. remote: Compressing objects: 100% (10/10), done. remote: Total 11 (delta 0), reused 11 (delta 0) Unpacking objects: 100% (11/11), done. Checking connectivity… done. Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'
Nếu bạn đã sao chép dự án và quên `--recurse-submodules`, bạn có thể kết hợp các bước `git submodule init` và `git submodule update` bằng cách chạy `git submodule update --init`. Để cũng khởi tạo, fetch và checkout bất kỳ mô-đun con lồng nhau nào, bạn có thể sử dụng `git submodule update --init --recursive` chắc chắn. ==== Làm việc trên một Dự án với các Mô-đun con Bây giờ chúng ta có một bản sao của một dự án với các mô-đun con trong đó và sẽ cộng tác với các đồng đội của mình trên cả dự án chính và dự án mô-đun con. ===== Kéo vào các Thay đổi Thượng nguồn từ Điều khiển từ xa Mô-đun con Mô hình đơn giản nhất của việc sử dụng các mô-đun con trong một dự án sẽ là nếu bạn chỉ đơn giản là tiêu thụ một dự án con và muốn nhận các bản cập nhật từ nó theo thời gian nhưng không thực sự sửa đổi bất cứ điều gì trong bản checkout của bạn. Hãy đi qua một ví dụ đơn giản ở đó. Nếu bạn muốn kiểm tra công việc mới trong một mô-đun con, bạn có thể đi vào thư mục và chạy `git fetch` và `git merge` nhánh thượng nguồn để cập nhật mã cục bộ. [source,console]
$ git fetch
From https://github.com/chaconinc/DbConnector
c3f01dc..d0354fc master → origin/master
$ git merge origin/master
Updating c3f01dc..d0354fc
Fast-forward
scripts/connect.sh | 1
src/db.c | 1
2 files changed, 2 insertions(+)
Bây giờ nếu bạn quay lại dự án chính và chạy `git diff --submodule`, bạn có thể thấy rằng mô-đun con đã được cập nhật và nhận danh sách các cam kết đã được thêm vào nó. Nếu bạn không muốn gõ `--submodule` mỗi lần bạn chạy `git diff`, bạn có thể đặt nó làm định dạng mặc định bằng cách đặt giá trị cấu hình `diff.submodule` thành "`log`". [source,console]
$ git config --global diff.submodule log $ git diff Submodule DbConnector c3f01dc..d0354fc: > more efficient db routine > better connection routine
Nếu bạn cam kết tại thời điểm này thì bạn sẽ khóa mô-đun con vào việc có mã mới khi những người khác cập nhật. Cũng có một cách dễ dàng hơn để làm điều này, nếu bạn muốn không phải fetch và merge thủ công trong thư mục con. Nếu bạn chạy `git submodule update --remote`, Git sẽ đi vào các mô-đun con của bạn và fetch và cập nhật cho bạn. [source,console]
$ git submodule update --remote DbConnector remote: Counting objects: 4, done. remote: Compressing objects: 100% (2/2), done. remote: Total 4 (delta 2), reused 4 (delta 2) Unpacking objects: 100% (4/4), done. From https://github.com/chaconinc/DbConnector 3f19983..d0354fc master → origin/master Submodule path 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644'
Lệnh này theo mặc định sẽ giả định rằng bạn muốn cập nhật checkout đến nhánh mặc định của kho lưu trữ mô-đun con từ xa (nhánh được trỏ bởi `HEAD` trên điều khiển từ xa). Tuy nhiên, bạn có thể đặt điều này thành một cái gì đó khác nếu bạn muốn. Ví dụ, nếu bạn muốn mô-đun con `DbConnector` theo dõi nhánh "`stable`" của kho lưu trữ đó, bạn có thể đặt nó trong tệp `.gitmodules` của mình (để mọi người khác cũng theo dõi nó), hoặc chỉ trong tệp `.git/config` cục bộ của bạn. Hãy đặt nó trong tệp `.gitmodules`: [source,console]
$ git config -f .gitmodules submodule.DbConnector.branch stable
$ git submodule update --remote remote: Counting objects: 4, done. remote: Compressing objects: 100% (2/2), done. remote: Total 4 (delta 2), reused 4 (delta 2) Unpacking objects: 100% (4/4), done. From https://github.com/chaconinc/DbConnector 27cf5d3..c87d55d stable → origin/stable Submodule path 'DbConnector': checked out 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687'
Nếu bạn bỏ qua `-f .gitmodules`, nó sẽ chỉ thực hiện thay đổi cho bạn, nhưng có lẽ hợp lý hơn khi theo dõi thông tin đó với kho lưu trữ để mọi người khác cũng làm như vậy. Khi chúng ta chạy `git status` tại thời điểm này, Git sẽ hiển thị cho chúng ta thấy rằng chúng ta có "`new commits`" (các cam kết mới) trên mô-đun con. [source,console]
$ git status On branch master Your branch is up-to-date with 'origin/master'.
Changes not staged for commit: (use "git add <file>…" to update what will be committed) (use "git checkout — <file>…" to discard changes in working directory)
modified: .gitmodules modified: DbConnector (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
Nếu bạn đặt cài đặt cấu hình `status.submodulesummary`, Git cũng sẽ hiển thị cho bạn một bản tóm tắt ngắn gọn về các thay đổi đối với các mô-đun con của bạn: [source,console]
$ git config status.submodulesummary 1
$ git status On branch master Your branch is up-to-date with 'origin/master'.
Changes not staged for commit: (use "git add <file>…" to update what will be committed) (use "git checkout — <file>…" to discard changes in working directory)
modified: .gitmodules modified: DbConnector (new commits)
Submodules changed but not updated:
-
DbConnector c3f01dc…c87d55d (4): > catch non-null terminated lines
Tại thời điểm này nếu bạn chạy `git diff`, chúng ta có thể thấy cả việc chúng ta đã sửa đổi tệp `.gitmodules` của mình và cũng có một số cam kết mà chúng ta đã kéo xuống và sẵn sàng cam kết vào dự án mô-đun con của mình. [source,console]
$ git diff diff --git a/.gitmodules b/.gitmodules index 6fc0b3d..fd1cc29 100644 --- a/.gitmodules + b/.gitmodules @@ -1,3 +1,4 @@ [submodule "DbConnector"] path = DbConnector url = https://github.com/chaconinc/DbConnector + branch = stable Submodule DbConnector c3f01dc..c87d55d: > catch non-null terminated lines > more robust error handling > more efficient db routine > better connection routine
Điều này khá tuyệt vì chúng ta thực sự có thể thấy nhật ký các cam kết mà chúng ta sắp cam kết trong mô-đun con của mình. Sau khi cam kết, bạn cũng có thể xem thông tin này sau khi thực tế khi bạn chạy `git log -p`. [source,console]
$ git log -p --submodule commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae Author: Scott Chacon <schacon@gmail.com> Date: Wed Sep 17 16:37:02 2014 +0200
updating DbConnector for bug fixes
diff --git a/.gitmodules b/.gitmodules index 6fc0b3d..fd1cc29 100644 --- a/.gitmodules + b/.gitmodules @@ -1,3 +1,4 @@ [submodule "DbConnector"] path = DbConnector url = https://github.com/chaconinc/DbConnector + branch = stable Submodule DbConnector c3f01dc..c87d55d: > catch non-null terminated lines > more robust error handling > more efficient db routine > better connection routine
Git theo mặc định sẽ cố gắng cập nhật *tất cả* các mô-đun con của bạn khi bạn chạy `git submodule update --remote`. Nếu bạn có nhiều mô-đun con, bạn có thể muốn chuyển tên của chỉ mô-đun con bạn muốn thử cập nhật. ===== Kéo các Thay đổi Thượng nguồn từ Điều khiển từ xa Dự án Bây giờ hãy bước vào vị trí của cộng tác viên của bạn, người có bản sao cục bộ riêng của kho lưu trữ MainProject. Chỉ cần thực hiện `git pull` để lấy các thay đổi mới được cam kết của bạn là không đủ: [source,console]
$ git pull From https://github.com/chaconinc/MainProject fb9093c..0a24cfc master → origin/master Fetching submodule DbConnector From https://github.com/chaconinc/DbConnector c3f01dc..c87d55d stable → origin/stable Updating fb9093c..0a24cfc Fast-forward .gitmodules | 2 - DbConnector | 2 +- 2 files changed, 2 insertions(), 2 deletions(-)
$ git status On branch master Your branch is up-to-date with 'origin/master'. Changes not staged for commit: (use "git add <file>…" to update what will be committed) (use "git checkout — <file>…" to discard changes in working directory)
modified: DbConnector (new commits)
Submodules changed but not updated:
-
DbConnector c87d55d…c3f01dc (4): < catch non-null terminated lines < more robust error handling < more efficient db routine < better connection routine
no changes added to commit (use "git add" and/or "git commit -a")
Theo mặc định, lệnh `git pull` fetch đệ quy các thay đổi mô-đun con, như chúng ta có thể thấy trong đầu ra của lệnh đầu tiên ở trên. Tuy nhiên, nó không *cập nhật* các mô-đun con. Điều này được hiển thị bởi đầu ra của lệnh `git status`, cho thấy mô-đun con bị "`modified`" (sửa đổi), và có "`new commits`" (cam kết mới). Hơn nữa, các dấu ngoặc hiển thị các cam kết mới trỏ sang trái (<), cho biết rằng các cam kết này được ghi lại trong MainProject nhưng không có trong bản checkout `DbConnector` cục bộ. Để hoàn tất cập nhật, bạn cần chạy `git submodule update`: [source,console]
$ git submodule update --init --recursive Submodule path 'vendor/plugins/demo': checked out '48679c6302815f6c76f1fe30625d795d9e55fc56'
$ git status On branch master Your branch is up-to-date with 'origin/master'. nothing to commit, working tree clean
Lưu ý rằng để an toàn, bạn nên chạy `git submodule update` với cờ `--init` trong trường hợp các cam kết MainProject bạn vừa kéo đã thêm các mô-đun con mới, và với cờ `--recursive` nếu bất kỳ mô-đun con nào có các mô-đun con lồng nhau. Nếu bạn muốn tự động hóa quá trình này, bạn có thể thêm cờ `--recurse-submodules` vào lệnh `git pull` (kể từ Git 2.14). Điều này sẽ làm cho Git chạy `git submodule update` ngay sau khi pull, đưa các mô-đun con vào trạng thái chính xác. Hơn nữa, nếu bạn muốn làm cho Git luôn pull với `--recurse-submodules`, bạn có thể đặt tùy chọn cấu hình `submodule.recurse` thành `true` (điều này hoạt động cho `git pull` kể từ Git 2.15). Tùy chọn này sẽ làm cho Git sử dụng cờ `--recurse-submodules` cho tất cả các lệnh hỗ trợ nó (ngoại trừ `clone`). Có một tình huống đặc biệt có thể xảy ra khi kéo các bản cập nhật siêu dự án: có thể là kho lưu trữ thượng nguồn đã thay đổi URL của mô-đun con trong tệp `.gitmodules` trong một trong các cam kết bạn kéo. Điều này có thể xảy ra ví dụ nếu dự án mô-đun con thay đổi nền tảng lưu trữ của nó. Trong trường hợp đó, có khả năng `git pull --recurse-submodules`, hoặc `git submodule update`, sẽ thất bại nếu siêu dự án tham chiếu đến một cam kết mô-đun con không được tìm thấy trong điều khiển từ xa mô-đun con được cấu hình cục bộ trong kho lưu trữ của bạn. Để khắc phục tình huống này, lệnh `git submodule sync` là bắt buộc: [source,console]
# copy the new URL to your local config $ git submodule sync --recursive # update the submodule from the new URL $ git submodule update --init --recursive
===== Làm việc trên một Mô-đun con Rất có khả năng là nếu bạn đang sử dụng các mô-đun con, bạn đang làm như vậy vì bạn thực sự muốn làm việc trên mã trong mô-đun con cùng lúc với bạn đang làm việc trên mã trong dự án chính (hoặc trên một vài mô-đun con). Nếu không, bạn có lẽ thay vào đó sẽ sử dụng một hệ thống quản lý phụ thuộc đơn giản hơn (chẳng hạn như Maven hoặc Rubygems). Vì vậy, bây giờ hãy đi qua một ví dụ về việc thực hiện các thay đổi đối với mô-đun con cùng lúc với dự án chính và cam kết và xuất bản các thay đổi đó cùng một lúc. Cho đến nay, khi chúng ta chạy lệnh `git submodule update` để fetch các thay đổi từ các kho lưu trữ mô-đun con, Git sẽ nhận các thay đổi và cập nhật các tệp trong thư mục con nhưng sẽ để lại kho lưu trữ con trong cái gọi là trạng thái "`detached HEAD`" (HEAD bị tách rời). Điều này có nghĩa là không có nhánh làm việc cục bộ nào (như `master`, chẳng hạn) theo dõi các thay đổi. Với việc không có nhánh làm việc nào theo dõi các thay đổi, điều đó có nghĩa là ngay cả khi bạn cam kết các thay đổi đối với mô-đun con, các thay đổi đó rất có thể sẽ bị mất lần sau khi bạn chạy `git submodule update`. Bạn phải thực hiện một số bước bổ sung nếu bạn muốn các thay đổi trong một mô-đun con được theo dõi. Để thiết lập mô-đun con của bạn để dễ dàng đi vào và hack, bạn cần làm hai việc. Bạn cần đi vào từng mô-đun con và checkout một nhánh để làm việc. Sau đó, bạn cần cho Git biết phải làm gì nếu bạn đã thực hiện các thay đổi và sau đó `git submodule update --remote` kéo vào công việc mới từ thượng nguồn. Các tùy chọn là bạn có thể hợp nhất chúng vào công việc cục bộ của mình, hoặc bạn có thể thử rebase công việc cục bộ của mình lên trên các thay đổi mới. Trước hết, hãy đi vào thư mục mô-đun con của chúng ta và checkout một nhánh. [source,console]
$ cd DbConnector/ $ git checkout stable Switched to branch 'stable'
Hãy thử cập nhật mô-đun con của chúng ta với tùy chọn "`merge`". Để chỉ định nó thủ công, chúng ta chỉ cần thêm tùy chọn `--merge` vào cuộc gọi `update` của chúng ta. Ở đây chúng ta sẽ thấy rằng đã có một thay đổi trên máy chủ cho mô-đun con này và nó được hợp nhất vào. [source,console]
$ cd ..
$ git submodule update --remote --merge
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
c87d55d..92c7337 stable → origin/stable
Updating c87d55d..92c7337
Fast-forward
src/main.c | 1
1 file changed, 1 insertion(+)
Submodule path 'DbConnector': merged in '92c7337b30ef9e0893e758dac2459d07362ab5ea'
Nếu chúng ta đi vào thư mục `DbConnector`, chúng ta có các thay đổi mới đã được hợp nhất vào nhánh `stable` cục bộ của chúng ta. Bây giờ hãy xem điều gì xảy ra khi chúng ta thực hiện thay đổi cục bộ của riêng mình đối với thư viện và ai đó khác đẩy một thay đổi khác lên thượng nguồn cùng một lúc. [source,console]
$ cd DbConnector/ $ vim src/db.c $ git commit -am 'Unicode support' [stable f906e16] Unicode support 1 file changed, 1 insertion(+)
Bây giờ nếu chúng ta cập nhật mô-đun con của mình, chúng ta có thể thấy điều gì xảy ra khi chúng ta đã thực hiện một thay đổi cục bộ và thượng nguồn cũng có một thay đổi mà chúng ta cần kết hợp. [source,console]
$ cd .. $ git submodule update --remote --rebase First, rewinding head to replay your work on top of it… Applying: Unicode support Submodule path 'DbConnector': rebased into '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'
Nếu bạn quên `--rebase` hoặc `--merge`, Git sẽ chỉ cập nhật mô-đun con thành bất cứ thứ gì có trên máy chủ và đặt lại dự án của bạn về trạng thái HEAD bị tách rời. [source,console]
$ git submodule update --remote Submodule path 'DbConnector': checked out '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'
Nếu điều này xảy ra, đừng lo lắng, bạn có thể đơn giản quay lại thư mục và checkout lại nhánh của mình (nhánh vẫn sẽ chứa công việc của bạn) và hợp nhất hoặc rebase `origin/stable` (hoặc bất kỳ nhánh từ xa nào bạn muốn) thủ công. Nếu bạn chưa cam kết các thay đổi của mình trong mô-đun con và bạn chạy một `submodule update` sẽ gây ra sự cố, Git sẽ fetch các thay đổi nhưng không ghi đè lên công việc chưa được lưu trong thư mục mô-đun con của bạn. [source,console]
$ git submodule update --remote remote: Counting objects: 4, done. remote: Compressing objects: 100% (3/3), done. remote: Total 4 (delta 0), reused 4 (delta 0) Unpacking objects: 100% (4/4), done. From https://github.com/chaconinc/DbConnector 5d60ef9..c75e92a stable → origin/stable error: Your local changes to the following files would be overwritten by checkout: scripts/setup.sh Please, commit your changes or stash them before you can switch branches. Aborting Unable to checkout 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'
Nếu bạn đã thực hiện các thay đổi xung đột với một cái gì đó đã thay đổi ở thượng nguồn, Git sẽ cho bạn biết khi bạn chạy cập nhật. [source,console]
$ git submodule update --remote --merge Auto-merging scripts/setup.sh CONFLICT (content): Merge conflict in scripts/setup.sh Recorded preimage for 'scripts/setup.sh' Automatic merge failed; fix conflicts and then commit the result. Unable to merge 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'
Bạn có thể đi vào thư mục mô-đun con và sửa xung đột giống như bạn thường làm. [[_publishing_submodules]] ===== Xuất bản Thay đổi Mô-đun con Bây giờ chúng ta có một số thay đổi trong thư mục mô-đun con của mình. Một số trong số này được đưa vào từ thượng nguồn bởi các bản cập nhật của chúng ta và những cái khác được thực hiện cục bộ và chưa có sẵn cho bất kỳ ai khác vì chúng ta chưa đẩy chúng. [source,console]
$ git diff Submodule DbConnector c87d55d..82d2ad3: > Merge from origin/stable > Update setup script > Unicode support > Remove unnecessary method > Add new option for conn pooling
Nếu chúng ta cam kết trong dự án chính và đẩy nó lên mà không đẩy các thay đổi mô-đun con lên, những người khác cố gắng checkout các thay đổi của chúng ta sẽ gặp rắc rối vì họ sẽ không có cách nào để lấy các thay đổi mô-đun con được phụ thuộc vào. Những thay đổi đó sẽ chỉ tồn tại trên bản sao cục bộ của chúng ta. Để đảm bảo điều này không xảy ra, bạn có thể yêu cầu Git kiểm tra xem tất cả các mô-đun con của bạn đã được đẩy đúng cách chưa trước khi đẩy dự án chính. Lệnh `git push` nhận đối số `--recurse-submodules` có thể được đặt thành "`check`" hoặc "`on-demand`". Tùy chọn "`check`" sẽ làm cho `push` đơn giản thất bại nếu bất kỳ thay đổi mô-đun con nào đã cam kết chưa được đẩy. [source,console]
$ git push --recurse-submodules=check The following submodule paths contain changes that can not be found on any remote: DbConnector
Please try
git push --recurse-submodules=on-demand
or cd to the path and use
git push
to push them to a remote.
Như bạn có thể thấy, nó cũng cung cấp cho chúng ta một số lời khuyên hữu ích về những gì chúng ta có thể muốn làm tiếp theo. Tùy chọn đơn giản là đi vào từng mô-đun con và đẩy thủ công đến các điều khiển từ xa để đảm bảo chúng có sẵn bên ngoài và sau đó thử đẩy lại lần này. Nếu bạn muốn hành vi "`check`" xảy ra cho tất cả các lần đẩy, bạn có thể đặt hành vi này làm mặc định bằng cách thực hiện `git config push.recurseSubmodules check`. Tùy chọn khác là sử dụng giá trị "`on-demand`", sẽ cố gắng làm điều này cho bạn. [source,console]
$ git push --recurse-submodules=on-demand Pushing submodule 'DbConnector' Counting objects: 9, done. Delta compression using up to 8 threads. Compressing objects: 100% (8/8), done. Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done. Total 9 (delta 3), reused 0 (delta 0) To https://github.com/chaconinc/DbConnector c75e92a..82d2ad3 stable → stable Counting objects: 2, done. Delta compression using up to 8 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done. Total 2 (delta 1), reused 0 (delta 0) To https://github.com/chaconinc/MainProject 3d6d338..9a377d1 master → master
Như bạn có thể thấy ở đó, Git đã đi vào mô-đun `DbConnector` và đẩy nó trước khi đẩy dự án chính. Nếu việc đẩy mô-đun con đó thất bại vì lý do nào đó, việc đẩy dự án chính cũng sẽ thất bại. Bạn có thể đặt hành vi này làm mặc định bằng cách thực hiện `git config push.recurseSubmodules on-demand`. ===== Hợp nhất Thay đổi Mô-đun con Nếu bạn thay đổi một tham chiếu mô-đun con cùng lúc với người khác, bạn có thể gặp phải một số vấn đề. Nghĩa là, nếu lịch sử mô-đun con đã phân kỳ và được cam kết vào các nhánh phân kỳ trong một siêu dự án, có thể mất một chút công sức để bạn sửa chữa. Nếu một trong các cam kết là tổ tiên trực tiếp của cái kia (hợp nhất fast-forward), thì Git sẽ đơn giản chọn cái sau cho việc hợp nhất, vì vậy điều đó hoạt động tốt. Tuy nhiên, Git sẽ không cố gắng thực hiện ngay cả một hợp nhất tầm thường cho bạn. Nếu các cam kết mô-đun con phân kỳ và cần được hợp nhất, bạn sẽ nhận được một cái gì đó trông giống như thế này: [source,console]
$ git pull remote: Counting objects: 2, done. remote: Compressing objects: 100% (1/1), done. remote: Total 2 (delta 1), reused 2 (delta 1) Unpacking objects: 100% (2/2), done. From https://github.com/chaconinc/MainProject 9a377d1..eb974f8 master → origin/master Fetching submodule DbConnector warning: Failed to merge submodule DbConnector (merge following commits not found) Auto-merging DbConnector CONFLICT (submodule): Merge conflict in DbConnector Automatic merge failed; fix conflicts and then commit the result.
Vì vậy, về cơ bản những gì đã xảy ra ở đây là Git đã tìm ra rằng hai nhánh ghi lại các điểm trong lịch sử của mô-đun con là phân kỳ và cần được hợp nhất. Nó giải thích nó là "`merge following commits not found`" (hợp nhất các cam kết sau không tìm thấy), điều này gây nhầm lẫn nhưng chúng ta sẽ giải thích tại sao lại như vậy một chút nữa. Để giải quyết vấn đề, bạn cần tìm ra trạng thái mà mô-đun con nên ở. Thật kỳ lạ, Git không thực sự cung cấp cho bạn nhiều thông tin để giúp đỡ ở đây, thậm chí không phải là các SHA-1 của các cam kết của cả hai bên lịch sử. May mắn thay, thật đơn giản để tìm ra. Nếu bạn chạy `git diff` bạn có thể nhận được các SHA-1 của các cam kết được ghi lại trong cả hai nhánh bạn đang cố gắng hợp nhất. [source,console]
$ git diff diff --cc DbConnector index eb41d76,c771610..0000000 --- a/DbConnector + b/DbConnector
Vì vậy, trong trường hợp này, `eb41d76` là cam kết trong mô-đun con của chúng ta mà *chúng ta* có và `c771610` là cam kết mà thượng nguồn có. Nếu chúng ta đi vào thư mục mô-đun con của mình, nó đã nên ở trên `eb41d76` vì việc hợp nhất sẽ không chạm vào nó. Nếu vì lý do nào đó nó không phải vậy, bạn có thể đơn giản tạo và checkout một nhánh trỏ đến nó. Điều quan trọng là SHA-1 của cam kết từ phía bên kia. Đây là những gì bạn sẽ phải hợp nhất vào và giải quyết. Bạn có thể chỉ cần thử hợp nhất với SHA-1 trực tiếp, hoặc bạn có thể tạo một nhánh cho nó và sau đó thử hợp nhất cái đó vào. Chúng tôi sẽ đề xuất cái sau, ngay cả khi chỉ để tạo một thông điệp cam kết hợp nhất đẹp hơn. Vì vậy, chúng ta sẽ đi vào thư mục mô-đun con của mình, tạo một nhánh có tên "`try-merge`" dựa trên SHA-1 thứ hai đó từ `git diff`, và hợp nhất thủ công. [source,console]
$ cd DbConnector
$ git rev-parse HEAD eb41d764bccf88be77aced643c13a7fa86714135
$ git branch try-merge c771610
$ git merge try-merge Auto-merging src/main.c CONFLICT (content): Merge conflict in src/main.c Recorded preimage for 'src/main.c' Automatic merge failed; fix conflicts and then commit the result.
Chúng ta đã gặp một xung đột hợp nhất thực sự ở đây, vì vậy nếu chúng ta giải quyết nó và cam kết nó, thì chúng ta có thể đơn giản cập nhật dự án chính với kết quả. [source,console]
$ vim src/main.c <1> $ git add src/main.c $ git commit -am 'merged our changes' Recorded resolution for 'src/main.c'. [master 9fd905e] merged our changes
$ cd .. <2> $ git diff <3> diff --cc DbConnector index eb41d76,c771610..0000000 --- a/DbConnector + b/DbConnector @@ -1,1 -1,1 +1,1 @@@ - Subproject commit eb41d764bccf88be77aced643c13a7fa86714135 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a $ git add DbConnector <4>
$ git commit -m "Merge Tom’s Changes" <5> [master 10d2c60] Merge Tom’s Changes
<1> Đầu tiên chúng ta giải quyết xung đột. <2> Sau đó chúng ta quay lại thư mục dự án chính. <3> Chúng ta có thể kiểm tra lại các SHA-1. <4> Giải quyết mục nhập mô-đun con bị xung đột. <5> Cam kết hợp nhất của chúng ta. Nó có thể hơi khó hiểu, nhưng nó thực sự không quá khó. Thú vị thay, có một trường hợp khác mà Git xử lý. Nếu một cam kết hợp nhất tồn tại trong thư mục mô-đun con chứa *cả hai* cam kết trong lịch sử của nó, Git sẽ đề xuất nó cho bạn như một giải pháp khả thi. Nó thấy rằng tại một thời điểm nào đó trong dự án mô-đun con, ai đó đã hợp nhất các nhánh chứa hai cam kết này, vì vậy có lẽ bạn sẽ muốn cái đó. Đây là lý do tại sao thông báo lỗi từ trước là "`merge following commits not found`", bởi vì nó không thể làm *điều này*. Thật khó hiểu vì ai sẽ mong đợi nó *cố gắng* làm điều này? Nếu nó tìm thấy một cam kết hợp nhất chấp nhận được duy nhất, bạn sẽ thấy một cái gì đó giống như thế này: [source,console]
$ git merge origin/master warning: Failed to merge submodule DbConnector (not fast-forward) Found a possible merge resolution for the submodule: 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes If this is correct simply add it to the index for example by using:
git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"
which will accept this suggestion. Auto-merging DbConnector CONFLICT (submodule): Merge conflict in DbConnector Automatic merge failed; fix conflicts and then commit the result.
Lệnh được đề xuất mà Git đang cung cấp sẽ cập nhật chỉ mục như thể bạn đã chạy `git add` (làm sạch xung đột), sau đó cam kết. Tuy nhiên, bạn có lẽ không nên làm điều này. Bạn có thể dễ dàng đi vào thư mục mô-đun con, xem sự khác biệt là gì, fast-forward đến cam kết này, kiểm tra nó đúng cách, và sau đó cam kết nó. [source,console]
$ cd DbConnector/ $ git merge 9fd905e Updating eb41d76..9fd905e Fast-forward
$ cd .. $ git add DbConnector $ git commit -am 'Fast forward to a common submodule child'
Điều này hoàn thành điều tương tự, nhưng ít nhất theo cách này bạn có thể xác minh rằng nó hoạt động và bạn có mã trong thư mục mô-đun con của mình khi bạn hoàn tất. ==== Mẹo Mô-đun con Có một vài điều bạn có thể làm để làm việc với các mô-đun con dễ dàng hơn một chút. ===== Submodule Foreach Có một lệnh mô-đun con `foreach` để chạy một lệnh tùy ý trong mỗi mô-đun con. Điều này có thể thực sự hữu ích nếu bạn có một số lượng mô-đun con trong cùng một dự án. Ví dụ, giả sử chúng ta muốn bắt đầu một tính năng mới hoặc sửa lỗi và chúng ta có công việc đang diễn ra trong một vài mô-đun con. Chúng ta có thể dễ dàng cất giữ (stash) tất cả công việc trong tất cả các mô-đun con của mình. [source,console]
$ git submodule foreach 'git stash' Entering 'CryptoLibrary' No local changes to save Entering 'DbConnector' Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable HEAD is now at 82d2ad3 Merge from origin/stable
Sau đó, chúng ta có thể tạo một nhánh mới và chuyển sang nó trong tất cả các mô-đun con của mình. [source,console]
$ git submodule foreach 'git checkout -b featureA' Entering 'CryptoLibrary' Switched to a new branch 'featureA' Entering 'DbConnector' Switched to a new branch 'featureA'
Bạn hiểu ý tưởng rồi đấy. Một điều thực sự hữu ích bạn có thể làm là tạo ra một diff thống nhất đẹp về những gì đã thay đổi trong dự án chính của bạn và tất cả các dự án con của bạn. [source,console]
$ git diff; git submodule foreach 'git diff' Submodule DbConnector contains modified content diff --git a/src/main.c b/src/main.c index 210f1ae..1f0acdc 100644 --- a/src/main.c + b/src/main.c @@ -245,6 +245,8 @@ static int handle_alias(int argcp, const char **argv)
commit_pager_choice();
+ url = url_decode(url_orig);
+ /* build alias_argv */ alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1)); alias_argv[0] = alias_string + 1; Entering 'DbConnector' diff --git a/src/db.c b/src/db.c index 1aaefb6..5297645 100644 --- a/src/db.c + b/src/db.c @@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len) return url_decode_internal(&url, len, NULL, &out, 0); }
+char *url_decode(const char *url) +{ + return url_decode_mem(url, strlen(url)); +}
+ char *url_decode_parameter_name(const char **query) { struct strbuf out = STRBUF_INIT;
Ở đây chúng ta có thể thấy rằng chúng ta đang định nghĩa một hàm trong một mô-đun con và gọi nó trong dự án chính. Đây rõ ràng là một ví dụ đơn giản hóa, nhưng hy vọng nó cung cấp cho bạn một ý tưởng về cách điều này có thể hữu ích. ===== Các Bí danh Hữu ích (Useful Aliases) Bạn có thể muốn thiết lập một số bí danh cho một số lệnh này vì chúng có thể khá dài và bạn không thể đặt các tùy chọn cấu hình cho hầu hết chúng để biến chúng thành mặc định. Chúng ta đã đề cập đến việc thiết lập các bí danh Git trong <<ch02-git-basics-chapter#_git_aliases>>, nhưng đây là một ví dụ về những gì bạn có thể muốn thiết lập nếu bạn dự định làm việc với các mô-đun con trong Git nhiều. [source,console]
$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'" $ git config alias.spush 'push --recurse-submodules=on-demand' $ git config alias.supdate 'submodule update --remote --merge'
Bằng cách này, bạn có thể chỉ cần chạy `git supdate` khi bạn muốn cập nhật các mô-đun con của mình, hoặc `git spush` để đẩy với kiểm tra phụ thuộc mô-đun con. ==== Các vấn đề với Mô-đun con Tuy nhiên, việc sử dụng các mô-đun con không phải là không có trục trặc. ===== Chuyển nhánh Ví dụ, chuyển nhánh với các mô-đun con trong đó cũng có thể khó khăn với các phiên bản Git cũ hơn Git 2.13. Nếu bạn tạo một nhánh mới, thêm một mô-đun con ở đó, và sau đó chuyển trở lại một nhánh không có mô-đun con đó, bạn vẫn có thư mục mô-đun con dưới dạng một thư mục không được theo dõi: [source,console]
$ git --version git version 2.12.2
$ git checkout -b add-crypto Switched to a new branch 'add-crypto'
$ git submodule add https://github.com/chaconinc/CryptoLibrary Cloning into 'CryptoLibrary'… …
$ git commit -am 'Add crypto library' [add-crypto 4445836] Add crypto library 2 files changed, 4 insertions(+) create mode 160000 CryptoLibrary
$ git checkout master warning: unable to rmdir CryptoLibrary: Directory not empty Switched to branch 'master' Your branch is up-to-date with 'origin/master'.
$ git status On branch master Your branch is up-to-date with 'origin/master'.
Untracked files: (use "git add <file>…" to include in what will be committed)
CryptoLibrary/
nothing added to commit but untracked files present (use "git add" to track)
Việc xóa thư mục không khó, nhưng có thể hơi khó hiểu khi có nó ở đó. Nếu bạn xóa nó và sau đó chuyển trở lại nhánh có mô-đun con đó, bạn sẽ cần chạy `submodule update --init` để điền lại nó. [source,console]
$ git clean -ffdx Removing CryptoLibrary/
$ git checkout add-crypto Switched to branch 'add-crypto'
$ ls CryptoLibrary/
$ git submodule update --init Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'
$ ls CryptoLibrary/ Makefile includes scripts src
Một lần nữa, không thực sự quá khó, nhưng nó có thể hơi khó hiểu. Các phiên bản Git mới hơn (Git >= 2.13) đơn giản hóa tất cả những điều này bằng cách thêm cờ `--recurse-submodules` vào lệnh `git checkout`, lệnh này sẽ lo việc đặt các mô-đun con vào trạng thái đúng cho nhánh chúng ta đang chuyển sang. [source,console]
$ git --version git version 2.13.3
$ git checkout -b add-crypto Switched to a new branch 'add-crypto'
$ git submodule add https://github.com/chaconinc/CryptoLibrary Cloning into 'CryptoLibrary'… …
$ git commit -am 'Add crypto library' [add-crypto 4445836] Add crypto library 2 files changed, 4 insertions(+) create mode 160000 CryptoLibrary
$ git checkout --recurse-submodules master Switched to branch 'master' Your branch is up-to-date with 'origin/master'.
$ git status On branch master Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean
Sử dụng cờ `--recurse-submodules` của `git checkout` cũng có thể hữu ích khi bạn làm việc trên một vài nhánh trong siêu dự án, mỗi nhánh có mô-đun con của bạn trỏ đến các cam kết khác nhau. Thật vậy, nếu bạn chuyển đổi giữa các nhánh ghi lại mô-đun con tại các cam kết khác nhau, khi thực thi `git status`, mô-đun con sẽ xuất hiện dưới dạng "`modified`" (đã sửa đổi), và chỉ ra "`new commits`" (cam kết mới). Đó là bởi vì trạng thái mô-đun con theo mặc định không được chuyển sang khi chuyển nhánh. Điều này có thể thực sự gây nhầm lẫn, vì vậy, một ý tưởng tốt là luôn `git checkout --recurse-submodules` khi dự án của bạn có các mô-đun con. Đối với các phiên bản Git cũ hơn không có cờ `--recurse-submodules`, sau khi checkout, bạn có thể sử dụng `git submodule update --init --recursive` để đưa các mô-đun con vào trạng thái đúng. May mắn thay, bạn có thể bảo Git (>=2.14) luôn sử dụng cờ `--recurse-submodules` bằng cách đặt tùy chọn cấu hình `submodule.recurse`: `git config submodule.recurse true`. Như đã lưu ý ở trên, điều này cũng sẽ làm cho Git đệ quy vào các mô-đun con cho mọi lệnh có tùy chọn `--recurse-submodules` (ngoại trừ `git clone`). ===== Chuyển từ thư mục con sang mô-đun con Cảnh báo chính khác mà nhiều người gặp phải liên quan đến việc chuyển từ thư mục con sang mô-đun con. Nếu bạn đang theo dõi các tệp trong dự án của mình và bạn muốn di chuyển chúng ra thành một mô-đun con, bạn phải cẩn thận nếu không Git sẽ nổi giận với bạn. Giả sử rằng bạn có các tệp trong một thư mục con của dự án của mình, và bạn muốn chuyển nó thành một mô-đun con. Nếu bạn xóa thư mục con và sau đó chạy `submodule add`, Git sẽ hét vào mặt bạn: [source,console]
$ rm -Rf CryptoLibrary/ $ git submodule add https://github.com/chaconinc/CryptoLibrary 'CryptoLibrary' already exists in the index
Bạn phải hủy tổ chức thư mục `CryptoLibrary` trước. Sau đó, bạn có thể thêm mô-đun con: [source,console]
$ git rm -r CryptoLibrary $ git submodule add https://github.com/chaconinc/CryptoLibrary Cloning into 'CryptoLibrary'… remote: Counting objects: 11, done. remote: Compressing objects: 100% (10/10), done. remote: Total 11 (delta 0), reused 11 (delta 0) Unpacking objects: 100% (11/11), done. Checking connectivity… done.
Bây giờ giả sử bạn đã làm điều đó trong một nhánh. Nếu bạn cố gắng chuyển trở lại một nhánh nơi các tệp đó vẫn nằm trong cây thực tế thay vì một mô-đun con -- bạn sẽ nhận được lỗi này: [source,console]
$ git checkout master error: The following untracked working tree files would be overwritten by checkout: CryptoLibrary/Makefile CryptoLibrary/includes/crypto.h … Please move or remove them before you can switch branches. Aborting
Bạn có thể buộc nó chuyển đổi với `checkout -f`, nhưng hãy cẩn thận rằng bạn không có các thay đổi chưa được lưu trong đó vì chúng có thể bị ghi đè bằng lệnh đó. [source,console]
$ git checkout -f master warning: unable to rmdir CryptoLibrary: Directory not empty Switched to branch 'master'
Sau đó, khi bạn chuyển trở lại, bạn nhận được một thư mục `CryptoLibrary` trống vì lý do nào đó và `git submodule update` cũng có thể không sửa được nó. Bạn có thể cần phải đi vào thư mục mô-đun con của mình và chạy `git checkout .` để lấy lại tất cả các tệp của mình. Bạn có thể chạy lệnh này trong một tập lệnh `submodule foreach` để chạy nó cho nhiều mô-đun con. Điều quan trọng cần lưu ý là các mô-đun con ngày nay giữ tất cả dữ liệu Git của chúng trong thư mục `.git` của dự án hàng đầu, vì vậy không giống như các phiên bản Git cũ hơn nhiều, việc phá hủy một thư mục mô-đun con sẽ không làm mất bất kỳ cam kết hoặc nhánh nào mà bạn đã có. Với các công cụ này, các mô-đun con có thể là một phương pháp khá đơn giản và hiệu quả để phát triển trên một vài dự án liên quan nhưng vẫn riêng biệt cùng một lúc. [[_bundling]] === Đóng gói Mặc dù chúng ta đã đề cập đến các cách phổ biến để truyền dữ liệu Git qua mạng (HTTP, SSH, v.v.), thực ra còn một cách nữa để làm điều đó không được sử dụng phổ biến nhưng thực sự có thể khá hữu ích. Git có khả năng "`đóng gói`" dữ liệu của mình vào một tệp duy nhất. Điều này có thể hữu ích trong nhiều tình huống khác nhau. Có thể mạng của bạn bị hỏng và bạn muốn gửi các thay đổi cho đồng nghiệp của mình. Có lẽ bạn đang làm việc ở một nơi xa và không có quyền truy cập vào mạng cục bộ vì lý do bảo mật. Có thể card không dây/ethernet của bạn vừa bị hỏng. Có thể bạn không có quyền truy cập vào một máy chủ được chia sẻ vào lúc này, bạn muốn gửi email cập nhật cho ai đó và bạn không muốn chuyển 40 cam kết qua `format-patch`. Đây là lúc lệnh `git bundle` có thể hữu ích. Lệnh `bundle` sẽ đóng gói mọi thứ thường được đẩy qua dây bằng lệnh `git push` vào một tệp nhị phân mà bạn có thể gửi email cho ai đó hoặc đặt trên một ổ đĩa flash, sau đó giải nén vào một kho lưu trữ khác. Hãy xem một ví dụ đơn giản. Giả sử bạn có một kho lưu trữ với hai cam kết: [source,console]
$ git log commit 9a466c572fe88b195efd356c3f2bbeccdb504102 Author: Scott Chacon <schacon@gmail.com> Date: Wed Mar 10 07:34:10 2010 -0800
Second commit
commit b1ec3248f39900d2a406049d762aa68e9641be25 Author: Scott Chacon <schacon@gmail.com> Date: Wed Mar 10 07:34:01 2010 -0800
First commit
Nếu bạn muốn gửi kho lưu trữ đó cho ai đó và bạn không có quyền truy cập vào một kho lưu trữ để đẩy đến, hoặc đơn giản là không muốn thiết lập một kho lưu trữ, bạn có thể đóng gói nó bằng `git bundle create`. [source,console]
$ git bundle create repo.bundle HEAD master Counting objects: 6, done. Delta compression using up to 2 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (6/6), 441 bytes, done. Total 6 (delta 0), reused 0 (delta 0)
Bây giờ bạn có một tệp có tên `repo.bundle` có tất cả dữ liệu cần thiết để tạo lại nhánh `master` của kho lưu trữ. Với lệnh `bundle`, bạn cần liệt kê mọi tham chiếu hoặc phạm vi cam kết cụ thể mà bạn muốn được bao gồm. Nếu bạn dự định điều này sẽ được sao chép ở nơi khác, bạn cũng nên thêm HEAD làm tham chiếu như chúng ta đã làm ở đây. Bạn có thể gửi email tệp `repo.bundle` này cho người khác, hoặc đặt nó trên một ổ USB và mang nó đi. Ở phía bên kia, giả sử bạn được gửi tệp `repo.bundle` này và muốn làm việc trên dự án. Bạn có thể sao chép từ tệp nhị phân vào một thư mục, giống như bạn làm từ một URL. [source,console]
$ git clone repo.bundle repo Cloning into 'repo'… … $ cd repo $ git log --oneline 9a466c5 Second commit b1ec324 First commit
Nếu bạn không bao gồm HEAD trong các tham chiếu, bạn cũng phải chỉ định `-b master` hoặc bất kỳ nhánh nào được bao gồm vì nếu không nó sẽ không biết nhánh nào để kiểm tra. Bây giờ giả sử bạn thực hiện ba cam kết trên đó và muốn gửi lại các cam kết mới qua một gói trên một thanh USB hoặc email. [source,console]
$ git log --oneline 71b84da Last commit - second repo c99cf5b Fourth commit - second repo 7011d3d Third commit - second repo 9a466c5 Second commit b1ec324 First commit
Đầu tiên chúng ta cần xác định phạm vi các cam kết chúng ta muốn bao gồm trong gói. Không giống như các giao thức mạng tự động tìm ra tập hợp dữ liệu tối thiểu để truyền qua mạng cho chúng ta, chúng ta sẽ phải tự tìm ra điều này. Bây giờ, bạn có thể chỉ cần làm điều tương tự và đóng gói toàn bộ kho lưu trữ, điều này sẽ hoạt động, nhưng tốt hơn là chỉ đóng gói sự khác biệt - chỉ ba cam kết chúng ta vừa thực hiện cục bộ. Để làm điều đó, bạn sẽ phải tính toán sự khác biệt. Như chúng ta đã mô tả trong <<ch07-git-tools#_commit_ranges>>, bạn có thể chỉ định một phạm vi các cam kết theo một số cách. Để có được ba cam kết mà chúng ta có trong nhánh `master` của mình mà không có trong nhánh chúng ta đã sao chép ban đầu, chúng ta có thể sử dụng một cái gì đó như `origin/master..master` hoặc `master ^origin/master`. Bạn có thể kiểm tra điều đó bằng lệnh `log`. [source,console]
$ git log --oneline master ^origin/master 71b84da Last commit - second repo c99cf5b Fourth commit - second repo 7011d3d Third commit - second repo
Vì vậy, bây giờ chúng ta đã có danh sách các cam kết chúng ta muốn bao gồm trong gói, hãy đóng gói chúng lại. Chúng ta làm điều đó bằng lệnh `git bundle create`, cung cấp cho nó một tên tệp chúng ta muốn gói của mình và phạm vi các cam kết chúng ta muốn đi vào đó. [source,console]
$ git bundle create commits.bundle master ^9a466c5 Counting objects: 11, done. Delta compression using up to 2 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (9/9), 775 bytes, done. Total 9 (delta 0), reused 0 (delta 0)
Bây giờ chúng ta có một tệp `commits.bundle` trong thư mục của mình. Nếu chúng ta lấy tệp đó và gửi cho đối tác của mình, cô ấy sau đó có thể nhập nó vào kho lưu trữ ban đầu, ngay cả khi nhiều công việc hơn đã được thực hiện ở đó trong khi chờ đợi. Khi cô ấy nhận được gói, cô ấy có thể kiểm tra nó để xem nó chứa gì trước khi nhập nó vào kho lưu trữ của mình. Lệnh đầu tiên là lệnh `bundle verify` sẽ đảm bảo tệp thực sự là một gói Git hợp lệ và bạn có tất cả các tổ tiên cần thiết để tái tạo nó một cách chính xác. [source,console]
$ git bundle verify ../commits.bundle The bundle contains 1 ref 71b84daaf49abed142a373b6e5c59a22dc6560dc refs/heads/master The bundle requires these 1 ref 9a466c572fe88b195efd356c3f2bbeccdb504102 second commit ../commits.bundle is okay
Nếu người đóng gói đã tạo một gói chỉ có hai cam kết cuối cùng họ đã thực hiện, thay vì cả ba, kho lưu trữ ban đầu sẽ không thể nhập nó, vì nó thiếu lịch sử cần thiết. Lệnh `verify` sẽ trông như thế này thay thế: [source,console]
$ git bundle verify ../commits-bad.bundle error: Repository lacks these prerequisite commits: error: 7011d3d8fc200abe0ad561c011c3852a4b7bbe95 Third commit - second repo
Tuy nhiên, gói đầu tiên của chúng ta là hợp lệ, vì vậy chúng ta có thể tìm nạp các cam kết từ nó. Nếu bạn muốn xem các nhánh nào có trong gói có thể được nhập, cũng có một lệnh để chỉ liệt kê các đầu: [source,console]
$ git bundle list-heads ../commits.bundle 71b84daaf49abed142a373b6e5c59a22dc6560dc refs/heads/master
Lệnh con `verify` cũng sẽ cho bạn biết các đầu. Mục đích là để xem những gì có thể được kéo vào, vì vậy bạn có thể sử dụng các lệnh `fetch` hoặc `pull` để nhập các cam kết từ gói này. Ở đây chúng ta sẽ tìm nạp nhánh `master` của gói vào một nhánh có tên `other-master` trong kho lưu trữ của chúng ta: [source,console]
$ git fetch ../commits.bundle master:other-master From ../commits.bundle * [new branch] master → other-master
Bây giờ chúng ta có thể thấy rằng chúng ta có các cam kết đã nhập trên nhánh `other-master` cũng như bất kỳ cam kết nào chúng ta đã thực hiện trong khi chờ đợi trong nhánh `master` của riêng mình. [source,console]
$ git log --oneline --decorate --graph --all * 8255d41 (HEAD, master) Third commit - first repo | * 71b84da (other-master) Last commit - second repo | * c99cf5b Fourth commit - second repo | * 7011d3d Third commit - second repo |/ * 9a466c5 Second commit * b1ec324 First commit
Vì vậy, `git bundle` có thể thực sự hữu ích để chia sẻ hoặc thực hiện các hoạt động kiểu mạng khi bạn không có mạng hoặc kho lưu trữ được chia sẻ phù hợp để làm điều đó. [[_replace]] === Thay thế Như chúng ta đã nhấn mạnh trước đây, các đối tượng trong cơ sở dữ liệu đối tượng của Git là không thể thay đổi, nhưng Git cung cấp một cách thú vị để _giả vờ_ thay thế các đối tượng trong cơ sở dữ liệu của nó bằng các đối tượng khác. Lệnh `replace` cho phép bạn chỉ định một đối tượng trong Git và nói "mỗi khi bạn tham chiếu đến đối tượng _này_, hãy giả vờ đó là một đối tượng _khác_". Điều này hữu ích nhất để thay thế một cam kết trong lịch sử của bạn bằng một cam kết khác mà không cần phải xây dựng lại toàn bộ lịch sử bằng, chẳng hạn, `git filter-branch`. Ví dụ, giả sử bạn có một lịch sử mã khổng lồ và muốn chia kho lưu trữ của mình thành một lịch sử ngắn cho các nhà phát triển mới và một lịch sử dài hơn và lớn hơn nhiều cho những người quan tâm đến khai thác dữ liệu. Bạn có thể ghép một lịch sử này vào lịch sử khác bằng cách "thay thế" cam kết sớm nhất trong dòng mới bằng cam kết cuối cùng trong dòng cũ hơn. Điều này rất hay vì nó có nghĩa là bạn không thực sự phải viết lại mọi cam kết trong lịch sử mới, như bạn thường phải làm để nối chúng lại với nhau (vì cha mẹ ảnh hưởng đến các SHA-1). Hãy thử điều này. Hãy lấy một kho lưu trữ hiện có, chia nó thành hai kho lưu trữ, một gần đây và một lịch sử, và sau đó chúng ta sẽ xem cách chúng ta có thể kết hợp lại chúng mà không cần sửa đổi các giá trị SHA-1 của các kho lưu trữ gần đây qua `replace`. Chúng ta sẽ sử dụng một kho lưu trữ đơn giản với năm cam kết đơn giản: [source,console]
$ git log --oneline ef989d8 Fifth commit c6e1e95 Fourth commit 9c68fdc Third commit 945704c Second commit c1822cf First commit
Chúng ta muốn chia cái này thành hai dòng lịch sử. Một dòng đi từ cam kết một đến cam kết bốn - đó sẽ là lịch sử. Dòng thứ hai sẽ chỉ là các cam kết bốn và năm - đó sẽ là lịch sử gần đây. .Ví dụ về lịch sử Git image::images/replace1.png[Ví dụ về lịch sử Git] Vâng, tạo lịch sử lịch sử rất dễ dàng, chúng ta có thể chỉ cần đặt một nhánh trong lịch sử và sau đó đẩy nhánh đó vào nhánh `master` của một kho lưu trữ từ xa mới. [source,console]
$ git branch history c6e1e95 $ git log --oneline --decorate ef989d8 (HEAD, master) Fifth commit c6e1e95 (history) Fourth commit 9c68fdc Third commit 945704c Second commit c1822cf First commit
.Tạo một nhánh `history` mới image::images/replace2.png[Tạo một nhánh `history` mới] Bây giờ chúng ta có thể đẩy nhánh `history` mới vào nhánh `master` của kho lưu trữ mới của chúng ta: [source,console]
$ git remote add project-history https://github.com/schacon/project-history $ git push project-history history:master Counting objects: 12, done. Delta compression using up to 2 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (12/12), 907 bytes, done. Total 12 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (12/12), done. To git@github.com:schacon/project-history.git * [new branch] history → master
OK, vì vậy lịch sử của chúng ta đã được xuất bản. Bây giờ phần khó hơn là cắt bớt lịch sử gần đây của chúng ta để nó nhỏ hơn. Chúng ta cần một sự chồng chéo để chúng ta có thể thay thế một cam kết này bằng một cam kết tương đương trong cam kết kia, vì vậy chúng ta sẽ cắt bớt cái này chỉ còn các cam kết bốn và năm (vì vậy cam kết bốn chồng chéo). [source,console]
$ git log --oneline --decorate ef989d8 (HEAD, master) Fifth commit c6e1e95 (history) Fourth commit 9c68fdc Third commit 945704c Second commit c1822cf First commit
Trong trường hợp này, rất hữu ích khi tạo một cam kết cơ sở có hướng dẫn về cách mở rộng lịch sử, để các nhà phát triển khác biết phải làm gì nếu họ gặp cam kết đầu tiên trong lịch sử bị cắt bớt và cần thêm. Vì vậy, những gì chúng ta sẽ làm là tạo một đối tượng cam kết ban đầu làm điểm cơ sở của chúng ta với các hướng dẫn, sau đó rebase các cam kết còn lại (bốn và năm) lên trên nó. Để làm điều đó, chúng ta cần chọn một điểm để chia, đó là cam kết thứ ba, là `9c68fdc` trong SHA-speak. Vì vậy, cam kết cơ sở của chúng ta sẽ dựa trên cây đó. Chúng ta có thể tạo cam kết cơ sở của mình bằng lệnh `commit-tree`, lệnh này chỉ nhận một cây và sẽ trả về cho chúng ta một đối tượng cam kết SHA-1 hoàn toàn mới, không có cha mẹ. [source,console]
$ echo 'Get history from blah blah blah' | git commit-tree 9c68fdc^{tree} 622e88e9cbfbacfb75b5279245b9fb38dfea10cf
[NOTE] ===== Lệnh `commit-tree` là một trong một tập hợp các lệnh thường được gọi là các lệnh 'plumbing'. Đây là các lệnh không được thiết kế để sử dụng trực tiếp, mà thay vào đó được các lệnh Git *khác* sử dụng để thực hiện các công việc nhỏ hơn. Trong những trường hợp chúng ta đang làm những điều kỳ lạ như thế này, chúng cho phép chúng ta làm những việc cấp thấp thực sự nhưng không dành cho việc sử dụng hàng ngày. Bạn có thể đọc thêm về các lệnh plumbing trong <<ch10-git-internals#_plumbing_porcelain>>. ===== .Tạo một cam kết cơ sở bằng `commit-tree` image::images/replace3.png[Tạo một cam kết cơ sở bằng `commit-tree`] OK, vì vậy bây giờ chúng ta có một cam kết cơ sở, chúng ta có thể rebase phần còn lại của lịch sử của chúng ta lên trên đó bằng `git rebase --onto`. Đối số `--onto` sẽ là SHA-1 chúng ta vừa nhận được từ `commit-tree` và điểm rebase sẽ là cam kết thứ ba (cha của cam kết đầu tiên chúng ta muốn giữ lại, `9c68fdc`): [source,console]
$ git rebase --onto 622e88 9c68fdc First, rewinding head to replay your work on top of it… Applying: fourth commit Applying: fifth commit
.Rebase lịch sử trên đỉnh cam kết cơ sở image::images/replace4.png[Rebase lịch sử trên đỉnh cam kết cơ sở] OK, vì vậy bây giờ chúng ta đã viết lại lịch sử gần đây của chúng ta trên đỉnh một cam kết cơ sở dùng một lần mà bây giờ có hướng dẫn về cách tái tạo toàn bộ lịch sử nếu chúng ta muốn. Chúng ta có thể đẩy lịch sử mới đó vào một dự án mới và bây giờ khi mọi người sao chép kho lưu trữ đó, họ sẽ chỉ thấy hai cam kết gần đây nhất và sau đó là một cam kết cơ sở với hướng dẫn. Bây giờ hãy chuyển vai trò sang một người đang sao chép dự án lần đầu tiên muốn toàn bộ lịch sử. Để lấy dữ liệu lịch sử sau khi sao chép kho lưu trữ bị cắt bớt này, người ta sẽ phải thêm một remote thứ hai cho kho lưu trữ lịch sử và tìm nạp: [source,console]
$ git clone https://github.com/schacon/project $ cd project
$ git log --oneline master e146b5f Fifth commit 81a708d Fourth commit 622e88e Get history from blah blah blah
$ git remote add project-history https://github.com/schacon/project-history $ git fetch project-history From https://github.com/schacon/project-history * [new branch] master → project-history/master
Bây giờ cộng tác viên sẽ có các cam kết gần đây của họ trong nhánh `master` và các cam kết lịch sử trong nhánh `project-history/master`. [source,console]
$ git log --oneline master e146b5f Fifth commit 81a708d Fourth commit 622e88e Get history from blah blah blah
$ git log --oneline project-history/master c6e1e95 Fourth commit 9c68fdc Third commit 945704c Second commit c1822cf First commit
Để kết hợp chúng, bạn có thể chỉ cần gọi `git replace` với cam kết bạn muốn thay thế và sau đó là cam kết bạn muốn thay thế nó. Vì vậy, chúng ta muốn thay thế cam kết "thứ tư" trong nhánh `master` bằng cam kết "thứ tư" trong nhánh `project-history/master`: [source,console]
$ git replace 81a708d c6e1e95
Bây giờ, nếu bạn nhìn vào lịch sử của nhánh `master`, nó sẽ trông như thế này: [source,console]
$ git log --oneline master e146b5f Fifth commit 81a708d Fourth commit 9c68fdc Third commit 945704c Second commit c1822cf First commit
Tuyệt vời phải không? Mà không cần phải thay đổi tất cả các SHA-1 ngược dòng, chúng ta đã có thể thay thế một cam kết trong lịch sử của chúng ta bằng một cam kết hoàn toàn khác và tất cả các công cụ bình thường (`bisect`, `blame`, v.v.) sẽ hoạt động như chúng ta mong đợi. .Kết hợp các cam kết bằng `git replace` image::images/replace5.png[Kết hợp các cam kết bằng `git replace`] Điều thú vị là, nó vẫn hiển thị `81a708d` là SHA-1, mặc dù nó thực sự đang sử dụng dữ liệu cam kết `c6e1e95` mà chúng ta đã thay thế nó. Ngay cả khi bạn chạy một lệnh như `cat-file`, nó sẽ hiển thị dữ liệu đã thay thế: [source,console]
$ git cat-file -p 81a708d tree 7bc544cf438903b65ca9104a1e30345eee6c083d parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252 author Scott Chacon <schacon@gmail.com> 1268712581 -0700 committer Scott Chacon <schacon@gmail.com> 1268712581 -0700
fourth commit
Hãy nhớ rằng cha mẹ thực sự của `81a708d` là cam kết giữ chỗ của chúng ta (`622e88e`), chứ không phải `9c68fdce` như nó nói ở đây. Một điều thú vị khác là dữ liệu này được giữ trong các tham chiếu của chúng ta: [source,console]
$ git for-each-ref e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit refs/heads/master c6e1e95051d41771a649f3145423f8809d1a74d4 commit refs/remotes/history/master e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit refs/remotes/origin/HEAD e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit refs/remotes/origin/master c6e1e95051d41771a649f31423f8809d1a74d4 commit refs/replace/81a708dd0e167a3f691541c7a6463343bc457040
Điều này có nghĩa là dễ dàng chia sẻ bản thay thế của chúng ta với những người khác, bởi vì chúng ta có thể đẩy cái này lên máy chủ của chúng ta và những người khác có thể dễ dàng tải xuống nó. Điều này không hữu ích lắm trong kịch bản ghép lịch sử mà chúng ta đã xem xét ở đây (vì mọi người dù sao cũng sẽ tải xuống cả hai lịch sử, vậy tại sao lại tách chúng ra?) nhưng nó có thể hữu ích trong các trường hợp khác. [[_credential_caching]] === Lưu trữ Thông tin đăng nhập (((credentials))) (((git commands, credential))) Nếu bạn sử dụng giao thức SSH để kết nối với các remote, bạn có thể có một khóa không có mật khẩu, cho phép bạn truyền dữ liệu an toàn mà không cần nhập tên người dùng và mật khẩu. Tuy nhiên, điều này không thể thực hiện được với các giao thức HTTP -- mỗi kết nối cần tên người dùng và mật khẩu. Điều này thậm chí còn khó khăn hơn đối với các hệ thống có xác thực hai yếu tố, nơi mã thông báo bạn sử dụng làm mật khẩu được tạo ngẫu nhiên và không thể đọc được. May mắn thay, Git có một hệ thống thông tin đăng nhập có thể giúp ích điều này. Git có một vài tùy chọn được cung cấp sẵn: * Mặc định là không lưu vào bộ nhớ cache. Mỗi kết nối sẽ nhắc bạn nhập tên người dùng và mật khẩu. * Chế độ "`cache`" giữ thông tin đăng nhập trong bộ nhớ trong một khoảng thời gian nhất định. Không có mật khẩu nào được lưu trữ trên đĩa, và chúng sẽ bị xóa khỏi bộ nhớ cache sau 15 phút. * Chế độ "`store`" lưu thông tin đăng nhập vào một tệp văn bản thuần túy trên đĩa, và chúng không bao giờ hết hạn. Điều này có nghĩa là cho đến khi bạn thay đổi mật khẩu cho máy chủ Git, bạn sẽ không bao giờ phải nhập lại thông tin đăng nhập của mình. Nhược điểm của phương pháp này là mật khẩu của bạn được lưu trữ dưới dạng văn bản rõ ràng trong một tệp thuần túy trong thư mục chính của bạn. * Nếu bạn đang sử dụng macOS, Git đi kèm với chế độ "`osxkeychain`", lưu thông tin đăng nhập vào chuỗi khóa an toàn được đính kèm với tài khoản hệ thống của bạn. Phương pháp này lưu trữ thông tin đăng nhập trên đĩa, và chúng không bao giờ hết hạn, nhưng chúng được mã hóa bằng cùng một hệ thống lưu trữ chứng chỉ HTTPS và tự động điền của Safari. * Nếu bạn đang sử dụng Windows, bạn có thể bật tính năng *Git Credential Manager* khi cài đặt https://gitforwindows.org/[Git for Windows] hoặc cài đặt riêng https://github.com/git-ecosystem/git-credential-manager/releases/latest[GCM mới nhất] dưới dạng một dịch vụ độc lập. Điều này tương tự như trình trợ giúp "`osxkeychain`" được mô tả ở trên, nhưng sử dụng Windows Credential Store để kiểm soát thông tin nhạy cảm. Nó cũng có thể phục vụ thông tin đăng nhập cho WSL1 hoặc WSL2. Xem https://github.com/git-ecosystem/git-credential-manager#readme[Hướng dẫn cài đặt GCM] để biết thêm thông tin. Bạn có thể chọn một trong các phương pháp này bằng cách đặt giá trị cấu hình Git: [source,console]
$ git config --global credential.helper cache
Một số trình trợ giúp này có các tùy chọn. Trình trợ giúp "`store`" có thể nhận đối số `--file <path>`, tùy chỉnh nơi tệp văn bản thuần túy được lưu (mặc định là `~/.git-credentials`). Trình trợ giúp "`cache`" chấp nhận tùy chọn `--timeout <seconds>`, thay đổi khoảng thời gian daemon của nó được giữ chạy (mặc định là "`900`", hoặc 15 phút). Đây là một ví dụ về cách bạn sẽ cấu hình trình trợ giúp "`store`" với một tên tệp tùy chỉnh: [source,console]
$ git config --global credential.helper 'store --file ~/.my-credentials'
Git thậm chí còn cho phép bạn cấu hình một vài trình trợ giúp. Khi tìm kiếm thông tin đăng nhập cho một máy chủ cụ thể, Git sẽ truy vấn chúng theo thứ tự, và dừng lại sau khi câu trả lời đầu tiên được cung cấp. Khi lưu thông tin đăng nhập, Git sẽ gửi tên người dùng và mật khẩu đến _tất cả_ các trình trợ giúp trong danh sách, và chúng có thể chọn làm gì với chúng. Đây là những gì một `.gitconfig` sẽ trông như thế nào nếu bạn có một tệp thông tin đăng nhập trên một ổ đĩa USB, nhưng muốn sử dụng bộ nhớ cache trong bộ nhớ để tiết kiệm một số lần gõ nếu ổ đĩa không được cắm vào: [source,ini]
helper = store --file /mnt/thumbdrive/.git-credentials helper = cache --timeout 30000
==== Bên dưới lớp vỏ Tất cả điều này hoạt động như thế nào? Lệnh gốc của Git cho hệ thống trợ giúp thông tin đăng nhập là `git credential`, nhận một lệnh làm đối số, và sau đó nhiều đầu vào hơn thông qua stdin. Điều này có thể dễ hiểu hơn với một ví dụ. Giả sử rằng một trình trợ giúp thông tin đăng nhập đã được cấu hình, và trình trợ giúp đã lưu trữ thông tin đăng nhập cho `mygithost`. Đây là một phiên làm việc sử dụng lệnh "`fill`", được gọi khi Git đang cố gắng tìm thông tin đăng nhập cho một máy chủ: [source,console]
$ git credential fill <1> protocol=https <2> host=mygithost <3> protocol=https <4> host=mygithost username=bob password=s3cre7 $ git credential fill <5> protocol=https host=unknownhost
Username for 'https://unknownhost': bob Password for 'https://bob@unknownhost': protocol=https host=unknownhost username=bob password=s3cre7
<1> Đây là dòng lệnh khởi tạo tương tác.
<2> Git-credential sau đó đang chờ đầu vào trên stdin.
Chúng tôi cung cấp cho nó những gì chúng tôi biết: giao thức và tên máy chủ.
<3> Một dòng trống cho biết đầu vào đã hoàn tất, và hệ thống thông tin đăng nhập nên trả lời với những gì nó biết.
<4> Git-credential sau đó tiếp quản, và ghi vào stdout với các bit thông tin nó tìm thấy.
<5> Nếu không tìm thấy thông tin đăng nhập, Git hỏi người dùng tên người dùng và mật khẩu, và cung cấp chúng lại cho stdout gọi (ở đây chúng được đính kèm vào cùng một bảng điều khiển).
Hệ thống thông tin đăng nhập thực sự đang gọi một chương trình riêng biệt với chính Git; chương trình nào và cách nào phụ thuộc vào giá trị cấu hình `credential.helper`.
Có một số dạng nó có thể nhận:
[options="header"]
|======
| Giá trị cấu hình | Hành vi
| `foo` | Chạy `git-credential-foo`
| `foo -a --opt=bcd` | Chạy `git-credential-foo -a --opt=bcd`
| `/absolute/path/foo -xyz` | Chạy `/absolute/path/foo -xyz`
| `!f() { echo "password=s3cre7"; }; f` | Mã sau `!` được đánh giá trong shell
|======
Vì vậy, các trình trợ giúp được mô tả ở trên thực sự có tên là `git-credential-cache`, `git-credential-store`, v.v., và chúng ta có thể cấu hình chúng để nhận các đối số dòng lệnh.
Dạng chung cho điều này là "`git-credential-foo [args] <action>.`"
Giao thức stdin/stdout giống như git-credential, nhưng chúng sử dụng một tập hợp hành động hơi khác:
* `get` là một yêu cầu cho một cặp tên người dùng/mật khẩu.
* `store` là một yêu cầu để lưu một tập hợp thông tin đăng nhập trong bộ nhớ của trình trợ giúp này.
* `erase` xóa thông tin đăng nhập cho các thuộc tính đã cho khỏi bộ nhớ của trình trợ giúp này.
Đối với các hành động `store` và `erase`, không cần phản hồi (Git dù sao cũng bỏ qua nó).
Tuy nhiên, đối với hành động `get`, Git rất quan tâm đến những gì trình trợ giúp phải nói.
Nếu trình trợ giúp không biết gì hữu ích, nó có thể đơn giản thoát mà không có đầu ra, nhưng nếu nó biết, nó nên bổ sung thông tin được cung cấp bằng thông tin nó đã lưu trữ.
Đầu ra được coi như một loạt các câu lệnh gán; bất cứ điều gì được cung cấp sẽ thay thế những gì Git đã biết.
Đây là ví dụ tương tự từ trên, nhưng bỏ qua `git-credential` và đi thẳng đến `git-credential-store`:
[source,console]
$ git credential-store --file ~/git.store store <1> protocol=https host=mygithost username=bob password=s3cre7 $ git credential-store --file ~/git.store get <2> protocol=https host=mygithost
username=bob <3> password=s3cre7
<1> Ở đây chúng ta yêu cầu `git-credential-store` lưu một số thông tin đăng nhập: tên người dùng "`bob`" và mật khẩu "`s3cre7`" sẽ được sử dụng khi truy cập `https://mygithost`.
<2> Bây giờ chúng ta sẽ truy xuất các thông tin đăng nhập đó.
Chúng tôi cung cấp các phần của kết nối chúng tôi đã biết (`https://mygithost`), và một dòng trống.
<3> `git-credential-store` trả lời bằng tên người dùng và mật khẩu chúng tôi đã lưu trữ ở trên.
Đây là những gì tệp `~/git.store` trông như thế nào:
[source,ini]
Nó chỉ là một loạt các dòng, mỗi dòng chứa một URL được trang trí bằng thông tin đăng nhập. Các trình trợ giúp `osxkeychain` và `wincred` sử dụng định dạng gốc của các cửa hàng hỗ trợ của chúng, trong đó `cache` sử dụng định dạng trong bộ nhớ riêng của nó (mà không có quy trình nào khác có thể đọc). ==== Bộ nhớ cache Thông tin đăng nhập Tùy chỉnh Do `git-credential-store` và các trình trợ giúp khác là các chương trình riêng biệt với Git, không khó để nhận ra rằng _bất kỳ_ chương trình nào cũng có thể là trình trợ giúp thông tin đăng nhập Git. Các trình trợ giúp được cung cấp bởi Git bao gồm nhiều trường hợp sử dụng phổ biến, nhưng không phải tất cả. Ví dụ, giả sử nhóm của bạn có một số thông tin đăng nhập được chia sẻ với toàn bộ nhóm, có lẽ là để triển khai. Chúng được lưu trữ trong một thư mục chia sẻ, nhưng bạn không muốn sao chép chúng vào bộ nhớ cache thông tin đăng nhập của riêng mình, bởi vì chúng thay đổi thường xuyên. Không có trình trợ giúp hiện có nào bao gồm trường hợp này; hãy xem cần làm gì để viết trình trợ giúp của riêng chúng ta. Có một số tính năng chính mà chương trình này cần phải có: . Hành động duy nhất chúng ta cần chú ý là `get`; `store` và `erase` là các hoạt động ghi, vì vậy chúng ta sẽ chỉ thoát sạch khi chúng được nhận. . Định dạng tệp của tệp thông tin đăng nhập được chia sẻ giống như định dạng được sử dụng bởi `git-credential-store`. . Vị trí của tệp đó khá chuẩn, nhưng chúng ta nên cho phép người dùng truyền một đường dẫn tùy chỉnh trong trường hợp. Một lần nữa, chúng ta sẽ viết tiện ích mở rộng này bằng Ruby, nhưng bất kỳ ngôn ngữ nào cũng sẽ hoạt động miễn là Git có thể thực thi sản phẩm cuối cùng. Đây là mã nguồn đầy đủ của trình trợ giúp thông tin đăng nhập mới của chúng ta: [source,ruby]
#!/usr/bin/env ruby
require 'optparse'
path = File.expand_path '~/.git-credentials' # <1> OptionParser.new do |opts| opts.banner = 'USAGE: git-credential-read-only [options] <action>' opts.on('-f', '--file PATH', 'Specify path for backing store') do |argpath| path = File.expand_path argpath end end.parse!
exit(0) unless ARGV[0].downcase == 'get' # <2> exit(0) unless File.exist? path
known = {} # <3> while line = STDIN.gets break if line.strip == '' k,v = line.strip.split '=', 2 known[k] = v end
File.readlines(path).each do |fileline| # <4> prot,user,pass,host = fileline.scan(/^(.?):\/\/(.?):(.?)@(.)$/).first if prot == known['protocol'] and host == known['host'] and user == known['username'] then puts "protocol={prot}" puts "host={host}" puts "username={user}" puts "password={pass}" exit(0) end end
<1> Ở đây chúng ta phân tích các tùy chọn dòng lệnh, cho phép người dùng chỉ định tệp đầu vào.
Mặc định là `~/.git-credentials`.
<2> Chương trình này chỉ phản hồi nếu hành động là `get` và tệp hỗ trợ tồn tại.
<3> Vòng lặp này đọc từ stdin cho đến khi đạt đến dòng trống đầu tiên.
Các đầu vào được lưu trữ trong băm `known` để tham chiếu sau này.
<4> Vòng lặp này đọc nội dung của tệp lưu trữ, tìm kiếm các kết quả khớp.
Nếu giao thức, máy chủ và tên người dùng từ `known` khớp với dòng này, chương trình sẽ in kết quả ra stdout và thoát.
Chúng ta sẽ lưu trình trợ giúp của mình dưới dạng `git-credential-read-only`, đặt nó ở đâu đó trong `PATH` của chúng ta và đánh dấu nó là có thể thực thi.
Đây là những gì một phiên tương tác trông như thế nào:
[source,console]
$ git credential-read-only --file=/mnt/shared/creds get protocol=https host=mygithost username=bob
protocol=https host=mygithost username=bob password=s3cre7
Vì tên của nó bắt đầu bằng "`git-`", chúng ta có thể sử dụng cú pháp đơn giản cho giá trị cấu hình: [source,console]
$ git config --global credential.helper 'read-only --file /mnt/shared/creds'
Như bạn có thể thấy, việc mở rộng hệ thống này khá đơn giản, và có thể giải quyết một số vấn đề phổ biến cho bạn và nhóm của bạn. === Tóm tắt Bạn đã thấy một số công cụ nâng cao cho phép thao tác commit và vùng dàn một cách chính xác hơn. Khi gặp vấn đề, bạn sẽ dễ dàng xác định commit nào đã gây ra, khi nào và bởi ai. Nếu muốn dùng các dự án con trong dự án chính, bạn đã học cách xử lý nhu cầu đó. Ở giai đoạn này, bạn nên có thể làm hầu hết các thao tác Git cần thiết trên dòng lệnh hàng ngày và cảm thấy thoải mái khi làm việc với chúng. [[ch08-customizing-git]] == Tuỳ biến Git Cho tới nay, chúng ta đã trình bày các kiến thức cơ bản về cách Git hoạt động và cách sử dụng nó, đồng thời giới thiệu một số công cụ giúp bạn dùng Git dễ dàng và hiệu quả hơn. Trong chương này, chúng ta sẽ xem cách làm cho Git hoạt động theo cách tuỳ biến hơn, bằng cách giới thiệu một số thiết lập cấu hình quan trọng và hệ thống hook. Với những công cụ này, bạn dễ dàng điều chỉnh Git hoạt động đúng theo nhu cầu của bạn, công ty hoặc nhóm của bạn. [[_git_config]] === Cấu hình Git (((git commands, config))) Như bạn đã đọc sơ qua trong <<ch01-getting-started#ch01-getting-started>>, bạn có thể chỉ định các thiết lập cấu hình Git bằng lệnh `git config`. Một trong những điều đầu tiên bạn đã làm là thiết lập tên và địa chỉ email của mình: [source,console]
$ git config --global user.name "John Doe" $ git config --global user.email johndoe@example.com
Bây giờ bạn sẽ tìm hiểu một vài tùy chọn thú vị hơn mà bạn có thể thiết lập theo cách này để tùy chỉnh việc sử dụng Git của mình. Đầu tiên, hãy ôn lại nhanh: Git sử dụng một loạt các tập tin cấu hình để xác định hành vi không mặc định mà bạn có thể muốn. Nơi đầu tiên Git tìm các giá trị này là trong tập tin `[path]/etc/gitconfig` toàn hệ thống, chứa các thiết lập được áp dụng cho mọi người dùng trên hệ thống và tất cả các kho chứa của họ. Nếu bạn truyền tùy chọn `--system` cho `git config`, nó sẽ đọc và ghi từ tập tin này một cách cụ thể. Nơi tiếp theo Git tìm kiếm là tập tin `~/.gitconfig` (hoặc `~/.config/git/config`), tập tin này dành riêng cho từng người dùng. Bạn có thể làm cho Git đọc và ghi vào tập tin này bằng cách truyền tùy chọn `--global`. Cuối cùng, Git tìm kiếm các giá trị cấu hình trong tập tin cấu hình trong thư mục Git (`.git/config`) của bất kỳ kho chứa nào bạn đang sử dụng. Các giá trị này dành riêng cho kho chứa đó, và đại diện cho việc truyền tùy chọn `--local` cho `git config`. Nếu bạn không chỉ định cấp độ nào bạn muốn làm việc cùng, đây là mặc định. Mỗi "`cấp độ`" này (hệ thống, toàn cục, cục bộ) sẽ ghi đè các giá trị ở cấp độ trước đó, vì vậy các giá trị trong `.git/config` sẽ thắng các giá trị trong `[path]/etc/gitconfig`, chẳng hạn. [NOTE] ==== Các tập tin cấu hình của Git là văn bản thuần túy, vì vậy bạn cũng có thể thiết lập các giá trị này bằng cách chỉnh sửa thủ công tập tin và chèn cú pháp chính xác. Tuy nhiên, thường thì chạy lệnh `git config` sẽ dễ dàng hơn. ==== ==== Cấu hình Cơ bản cho Máy khách Các tùy chọn cấu hình được Git công nhận chia thành hai loại: phía máy khách và phía máy chủ. Phần lớn các tùy chọn là phía máy khách -- cấu hình các sở thích làm việc cá nhân của bạn. Rất, _rất_ nhiều tùy chọn cấu hình được hỗ trợ, nhưng một phần lớn trong số đó chỉ hữu ích trong một số trường hợp hiếm gặp; chúng tôi sẽ chỉ đề cập đến các tùy chọn phổ biến và hữu ích nhất ở đây. Nếu bạn muốn xem danh sách tất cả các tùy chọn mà phiên bản Git của bạn công nhận, bạn có thể chạy: [source,console]
$ man git-config
Lệnh này liệt kê tất cả các tùy chọn có sẵn khá chi tiết. Bạn cũng có thể tìm thấy tài liệu tham khảo này tại https://git-scm.com/docs/git-config[^]. [NOTE] ==== Đối với các trường hợp sử dụng nâng cao, bạn có thể muốn tra cứu "Conditional includes" (Bao gồm có điều kiện) trong tài liệu được đề cập ở trên. ==== ===== `core.editor` ((($EDITOR)))((($VISUAL, xem $EDITOR))) Theo mặc định, Git sử dụng bất kỳ thứ gì bạn đã đặt làm trình soạn thảo văn bản mặc định của mình thông qua một trong các biến môi trường shell `VISUAL` hoặc `EDITOR`, hoặc nếu không sẽ quay lại trình soạn thảo `vi` để tạo và chỉnh sửa các thông điệp commit và tag của bạn. Để thay đổi mặc định đó thành một cái gì đó khác, bạn có thể sử dụng thiết lập `core.editor`: [source,console]
$ git config --global core.editor emacs
Bây giờ, bất kể trình soạn thảo shell mặc định của bạn là gì, Git sẽ kích hoạt Emacs để chỉnh sửa tin nhắn. ===== `commit.template` (((commit templates))) Nếu bạn đặt cái này thành đường dẫn của một tập tin trên hệ thống của bạn, Git sẽ sử dụng tập tin đó làm tin nhắn ban đầu mặc định khi bạn commit. Giá trị của việc tạo một mẫu commit tùy chỉnh là bạn có thể sử dụng nó để nhắc nhở bản thân (hoặc người khác) về định dạng và phong cách phù hợp khi tạo một thông điệp commit. Ví dụ, hãy xem xét một tập tin mẫu tại `~/.gitmessage.txt` trông như thế này: [source,text]
Subject line (try to keep under 50 characters)
Multi-line description of commit, feel free to be detailed.
Lưu ý cách mẫu commit này nhắc nhở người commit giữ cho dòng chủ đề ngắn gọn (để phục vụ cho đầu ra của `git log --oneline`), thêm chi tiết bên dưới, và tham chiếu đến số phiếu theo dõi vấn đề hoặc lỗi nếu có. Để bảo Git sử dụng nó làm tin nhắn mặc định xuất hiện trong trình soạn thảo của bạn khi bạn chạy `git commit`, hãy đặt giá trị cấu hình `commit.template`: [source,console]
$ git config --global commit.template ~/.gitmessage.txt $ git commit
Sau đó, trình soạn thảo của bạn sẽ mở ra một cái gì đó giống như thế này cho thông điệp commit giữ chỗ của bạn khi bạn commit: [source,text]
Subject line (try to keep under 50 characters)
Multi-line description of commit, feel free to be detailed.
# Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: # (use "git reset HEAD <file>…" to unstage) # # modified: lib/test.rb # ~ ~ ".git/COMMIT_EDITMSG" 14L, 297C
Nếu đội ngũ của bạn có chính sách về thông điệp commit, thì việc đặt một mẫu cho chính sách đó trên hệ thống của bạn và cấu hình Git để sử dụng nó theo mặc định có thể giúp tăng cơ hội chính sách đó được tuân thủ thường xuyên. ===== `core.pager` (((pager))) Thiết lập này xác định trình phân trang nào được sử dụng khi Git phân trang đầu ra như `log` và `diff`. Bạn có thể đặt nó thành `more` hoặc trình phân trang yêu thích của bạn (mặc định là `less`), hoặc bạn có thể tắt nó bằng cách đặt nó thành một chuỗi trống: [source,console]
$ git config --global core.pager ''
Nếu bạn chạy lệnh đó, Git sẽ in toàn bộ đầu ra của tất cả các lệnh, bất kể chúng dài bao nhiêu. ===== `user.signingkey` (((GPG))) Nếu bạn đang tạo các thẻ (tag) có chú thích được ký (như đã thảo luận trong <<ch07-git-tools#_signing>>), việc thiết lập khóa ký GPG của bạn làm thiết lập cấu hình sẽ giúp mọi việc dễ dàng hơn. Thiết lập ID khóa của bạn như sau: [source,console]
$ git config --global user.signingkey <gpg-key-id>
Bây giờ, bạn có thể ký các thẻ mà không cần phải chỉ định khóa của mình mỗi lần với lệnh `git tag`: [source,console]
$ git tag -s <tag-name>
===== `core.excludesfile` (((excludes)))(((.gitignore))) Bạn có thể đặt các mẫu trong tập tin `.gitignore` của dự án để Git không xem chúng là các tập tin không được theo dõi hoặc cố gắng stage chúng khi bạn chạy `git add` trên chúng, như đã thảo luận trong <<ch02-git-basics-chapter#_ignoring>>. Nhưng đôi khi bạn muốn bỏ qua một số tập tin nhất định cho tất cả các kho chứa mà bạn làm việc cùng. Nếu máy tính của bạn đang chạy macOS, có lẽ bạn đã quen thuộc với các tập tin `.DS_Store`. Nếu trình soạn thảo ưa thích của bạn là Emacs hoặc Vim, bạn biết về các tên tập tin kết thúc bằng `~` hoặc `.swp`. Thiết lập này cho phép bạn viết một loại tập tin `.gitignore` toàn cục. Nếu bạn tạo một tập tin `~/.gitignore_global` với nội dung sau: [source,ini]
~ ..swp .DS_Store
…và bạn chạy `git config --global core.excludesfile ~/.gitignore_global`, Git sẽ không bao giờ làm phiền bạn về những tập tin đó nữa. ===== `help.autocorrect` (((autocorrect))) Nếu bạn gõ sai một lệnh, nó sẽ hiển thị cho bạn một cái gì đó như thế này: [source,console]
$ git chekcout master git: 'chekcout' is not a git command. See 'git --help'.
The most similar command is checkout
Git cố gắng tìm ra ý bạn muốn nói một cách hữu ích, nhưng nó vẫn từ chối thực hiện nó. Nếu bạn đặt `help.autocorrect` thành 1, Git sẽ thực sự chạy lệnh này cho bạn: [source,console]
$ git chekcout master WARNING: You called a Git command named 'chekcout', which does not exist. Continuing under the assumption that you meant 'checkout' in 0.1 seconds automatically…
Lưu ý cái vụ "`0.1 seconds`". `help.autocorrect` thực sự là một số nguyên đại diện cho phần mười của một giây. Vì vậy, nếu bạn đặt nó thành 50, Git sẽ cho bạn 5 giây để thay đổi ý định trước khi thực thi lệnh đã được tự động sửa. ==== Màu sắc trong Git (((color))) Git hỗ trợ đầy đủ đầu ra terminal có màu, điều này hỗ trợ rất nhiều trong việc phân tích cú pháp đầu ra lệnh một cách trực quan nhanh chóng và dễ dàng. Một số tùy chọn có thể giúp bạn thiết lập màu sắc theo sở thích của mình. ===== `color.ui` Git tự động tô màu hầu hết đầu ra của nó, nhưng có một công tắc tổng nếu bạn không thích hành vi này. Để tắt tất cả đầu ra terminal có màu của Git, hãy làm như sau: [source,console]
$ git config --global color.ui false
Thiết lập mặc định là `auto`, tô màu đầu ra khi nó đi thẳng đến terminal, nhưng bỏ qua các mã điều khiển màu khi đầu ra được chuyển hướng đến một đường ống (pipe) hoặc một tập tin. Bạn cũng có thể đặt nó thành `always` để bỏ qua sự khác biệt giữa terminal và đường ống. Bạn sẽ hiếm khi muốn điều này; trong hầu hết các tình huống, nếu bạn muốn mã màu trong đầu ra được chuyển hướng của mình, thay vào đó bạn có thể truyền cờ `--color` cho lệnh Git để buộc nó sử dụng mã màu. Thiết lập mặc định hầu như luôn là những gì bạn muốn. ===== `color.*` Nếu bạn muốn cụ thể hơn về lệnh nào được tô màu và như thế nào, Git cung cấp các thiết lập tô màu cụ thể cho từng động từ. Mỗi cái này có thể được đặt thành `true`, `false`, hoặc `always`: color.branch color.diff color.interactive color.status Ngoài ra, mỗi cái này có các thiết lập con bạn có thể sử dụng để đặt màu cụ thể cho các phần của đầu ra, nếu bạn muốn ghi đè từng màu. Ví dụ, để đặt thông tin meta trong đầu ra diff của bạn thành chữ màu xanh lam, nền đen và in đậm, bạn có thể chạy: [source,console]
$ git config --global color.diff.meta "blue black bold"
Bạn có thể đặt màu thành bất kỳ giá trị nào sau đây: `normal`, `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, hoặc `white`. Nếu bạn muốn một thuộc tính như in đậm trong ví dụ trước, bạn có thể chọn từ `bold`, `dim`, `ul` (gạch chân), `blink`, và `reverse` (hoán đổi nền và chữ). [[_external_merge_tools]] ==== Công cụ Trộn và So sánh Bên ngoài (((mergetool)))(((difftool))) Mặc dù Git có một triển khai nội bộ của diff, đó là những gì chúng tôi đã hiển thị trong cuốn sách này, bạn có thể thiết lập một công cụ bên ngoài thay thế. Bạn cũng có thể thiết lập một công cụ giải quyết xung đột trộn đồ họa thay vì phải giải quyết xung đột thủ công. Chúng tôi sẽ minh họa việc thiết lập Perforce Visual Merge Tool (P4Merge) để thực hiện diff và giải quyết trộn của bạn, bởi vì nó là một công cụ đồ họa đẹp và miễn phí. Nếu bạn muốn thử cái này, P4Merge hoạt động trên tất cả các nền tảng chính, vì vậy bạn sẽ có thể làm như vậy. Chúng tôi sẽ sử dụng tên đường dẫn trong các ví dụ hoạt động trên hệ thống macOS và Linux; đối với Windows, bạn sẽ phải thay đổi `/usr/local/bin` thành một đường dẫn thực thi trong môi trường của bạn. Để bắt đầu, https://www.perforce.com/products/helix-core-apps/merge-diff-tool-p4merge[tải xuống P4Merge từ Perforce^]. Tiếp theo, bạn sẽ thiết lập các tập tin kịch bản bao bọc (wrapper scripts) bên ngoài để chạy các lệnh của bạn. Chúng tôi sẽ sử dụng đường dẫn macOS cho tệp thực thi; trong các hệ thống khác, nó sẽ là nơi cài đặt nhị phân `p4merge` của bạn. Thiết lập một tập tin kịch bản bao bọc trộn có tên `extMerge` gọi nhị phân của bạn với tất cả các đối số được cung cấp: [source,console]
$ cat /usr/local/bin/extMerge #!/bin/sh /Applications/p4merge.app/Contents/MacOS/p4merge $*
Trình bao bọc diff kiểm tra để đảm bảo bảy đối số được cung cấp và chuyển hai trong số đó đến tập tin kịch bản trộn của bạn. Theo mặc định, Git chuyển các đối số sau cho chương trình diff: [source]
path old-file old-hex old-mode new-file new-hex new-mode
Bởi vì bạn chỉ muốn các đối số `old-file` và `new-file`, bạn sử dụng tập tin kịch bản bao bọc để chuyển những cái bạn cần. [source,console]
$ cat /usr/local/bin/extDiff !/bin/sh [ $ -eq 7 ] && /usr/local/bin/extMerge "$2" "$5"
Bạn cũng cần đảm bảo các công cụ này có thể thực thi được: [source,console]
$ sudo chmod +x /usr/local/bin/extMerge $ sudo chmod +x /usr/local/bin/extDiff
Bây giờ bạn có thể thiết lập tập tin cấu hình của mình để sử dụng các công cụ giải quyết trộn và diff tùy chỉnh của bạn. Điều này cần một số thiết lập tùy chỉnh: `merge.tool` để bảo Git chiến lược nào cần sử dụng, `mergetool.<tool>.cmd` để chỉ định cách chạy lệnh, `mergetool.<tool>.trustExitCode` để bảo Git biết mã thoát của chương trình đó có cho biết giải quyết trộn thành công hay không, và `diff.external` để bảo Git lệnh nào cần chạy cho diff. Vì vậy, bạn có thể chạy bốn lệnh cấu hình: [source,console]
$ git config --global merge.tool extMerge $ git config --global mergetool.extMerge.cmd \ 'extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"' $ git config --global mergetool.extMerge.trustExitCode false $ git config --global diff.external extDiff
hoặc bạn có thể chỉnh sửa tập tin `~/.gitconfig` của mình để thêm các dòng sau: [source,ini]
tool = extMerge
cmd = extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED" trustExitCode = false
external = extDiff
Sau khi tất cả những điều này được thiết lập, nếu bạn chạy các lệnh diff như thế này: [source,console]
$ git diff 32d1776b1^ 32d1776b1
Thay vì nhận đầu ra diff trên dòng lệnh, Git kích hoạt P4Merge, trông giống như thế này: .P4Merge image::images/p4merge.png[P4Merge] Nếu bạn cố gắng trộn hai nhánh và sau đó gặp xung đột trộn, bạn có thể chạy lệnh `git mergetool`; nó khởi động P4Merge để cho phép bạn giải quyết các xung đột thông qua công cụ GUI đó. Điều tuyệt vời về thiết lập bao bọc này là bạn có thể thay đổi các công cụ diff và trộn của mình một cách dễ dàng. Ví dụ, để thay đổi các công cụ `extDiff` và `extMerge` của bạn để chạy công cụ KDiff3 thay thế, tất cả những gì bạn phải làm là chỉnh sửa tập tin `extMerge` của mình: [source,console]
$ cat /usr/local/bin/extMerge #!/bin/sh /Applications/kdiff3.app/Contents/MacOS/kdiff3 $*
Bây giờ, Git sẽ sử dụng công cụ KDiff3 để xem diff và giải quyết xung đột trộn. Git được cài đặt sẵn để sử dụng một số công cụ giải quyết trộn khác mà bạn không cần phải thiết lập cấu hình cmd. Để xem danh sách các công cụ mà nó hỗ trợ, hãy thử: [source,console]
$ git mergetool --tool-help 'git mergetool --tool=<tool>' may be set to one of the following: emerge gvimdiff gvimdiff2 opendiff p4merge vimdiff vimdiff2
The following tools are valid, but not currently available: araxis bc3 codecompare deltawalker diffmerge diffuse ecmerge kdiff3 meld tkdiff tortoisemerge xxdiff
Some of the tools listed above only work in a windowed environment. If run in a terminal-only session, they will fail.
Nếu bạn không quan tâm đến việc sử dụng KDiff3 cho diff mà chỉ muốn sử dụng nó cho giải quyết trộn, và lệnh kdiff3 nằm trong đường dẫn của bạn, thì bạn có thể chạy: [source,console]
$ git config --global merge.tool kdiff3
Nếu bạn chạy lệnh này thay vì thiết lập các tập tin `extMerge` và `extDiff`, Git sẽ sử dụng KDiff3 để giải quyết trộn và công cụ diff Git bình thường cho diff. ==== Định dạng và Khoảng trắng (((whitespace))) Các vấn đề về định dạng và khoảng trắng là một trong những vấn đề khó chịu và tinh vi hơn mà nhiều nhà phát triển gặp phải khi cộng tác, đặc biệt là đa nền tảng. Rất dễ để các bản vá hoặc công việc cộng tác khác đưa vào các thay đổi khoảng trắng tinh vi vì các trình soạn thảo âm thầm đưa chúng vào, và nếu các tập tin của bạn từng chạm vào hệ thống Windows, kết thúc dòng của chúng có thể bị thay thế. Git có một vài tùy chọn cấu hình để giúp giải quyết các vấn đề này. ===== `core.autocrlf` (((crlf)))(((line endings))) Nếu bạn đang lập trình trên Windows và làm việc với những người không phải (hoặc ngược lại), bạn có thể sẽ gặp phải các vấn đề về kết thúc dòng tại một thời điểm nào đó. Điều này là do Windows sử dụng cả ký tự về đầu dòng (carriage-return) và ký tự xuống dòng (linefeed) cho các dòng mới trong các tập tin của nó, trong khi các hệ thống macOS và Linux chỉ sử dụng ký tự xuống dòng. Đây là một thực tế tinh vi nhưng cực kỳ khó chịu của công việc đa nền tảng; nhiều trình soạn thảo trên Windows âm thầm thay thế các kết thúc dòng kiểu LF hiện có bằng CRLF, hoặc chèn cả hai ký tự kết thúc dòng khi người dùng nhấn phím enter. Git có thể xử lý việc này bằng cách tự động chuyển đổi kết thúc dòng CRLF thành LF khi bạn thêm một tập tin vào chỉ mục, và ngược lại khi nó check out mã vào hệ thống tập tin của bạn. Bạn có thể bật chức năng này bằng thiết lập `core.autocrlf`. Nếu bạn đang ở trên máy Windows, hãy đặt nó thành `true` -- điều này chuyển đổi kết thúc LF thành CRLF khi bạn check out mã: [source,console]
$ git config --global core.autocrlf true
Nếu bạn đang ở trên hệ thống Linux hoặc macOS sử dụng kết thúc dòng LF, thì bạn không muốn Git tự động chuyển đổi chúng khi bạn check out tập tin; tuy nhiên, nếu một tập tin có kết thúc CRLF vô tình được đưa vào, thì bạn có thể muốn Git sửa nó. Bạn có thể bảo Git chuyển đổi CRLF thành LF khi commit nhưng không làm ngược lại bằng cách đặt `core.autocrlf` thành `input`: [source,console]
$ git config --global core.autocrlf input
Thiết lập này sẽ để lại cho bạn các kết thúc CRLF trong các lần check out trên Windows, nhưng kết thúc LF trên các hệ thống macOS và Linux và trong kho chứa. Nếu bạn là một lập trình viên Windows đang thực hiện một dự án chỉ dành cho Windows, thì bạn có thể tắt chức năng này, ghi lại các ký tự về đầu dòng trong kho chứa bằng cách đặt giá trị cấu hình thành `false`: [source,console]
$ git config --global core.autocrlf false
===== `core.whitespace` Git được cài đặt sẵn để phát hiện và sửa một số vấn đề về khoảng trắng. Nó có thể tìm kiếm sáu vấn đề khoảng trắng chính -- ba được bật theo mặc định và có thể tắt, và ba bị tắt theo mặc định nhưng có thể kích hoạt. Ba cái được bật theo mặc định là `blank-at-eol`, tìm kiếm khoảng trắng ở cuối dòng; `blank-at-eof`, nhận thấy các dòng trống ở cuối tập tin; và `space-before-tab`, tìm kiếm khoảng trắng trước tab ở đầu dòng. Ba cái bị tắt theo mặc định nhưng có thể bật là `indent-with-non-tab`, tìm kiếm các dòng bắt đầu bằng khoảng trắng thay vì tab (và được kiểm soát bởi tùy chọn `tabwidth`); `tab-in-indent`, theo dõi các tab trong phần thụt đầu dòng của một dòng; và `cr-at-eol`, bảo Git rằng các ký tự về đầu dòng ở cuối dòng là OK. Bạn có thể bảo Git cái nào trong số này bạn muốn bật bằng cách đặt `core.whitespace` thành các giá trị bạn muốn bật hoặc tắt, phân tách bằng dấu phẩy. Bạn có thể tắt một tùy chọn bằng cách thêm dấu `-` vào trước tên của nó, hoặc sử dụng giá trị mặc định bằng cách bỏ nó ra khỏi chuỗi thiết lập hoàn toàn. Ví dụ, nếu bạn muốn tất cả trừ `space-before-tab` được đặt, bạn có thể làm điều này (với `trailing-space` là cách viết tắt để bao gồm cả `blank-at-eol` và `blank-at-eof`): [source,console]
$ git config --global core.whitespace \ trailing-space,-space-before-tab,indent-with-non-tab,tab-in-indent,cr-at-eol
Hoặc bạn có thể chỉ định phần tùy chỉnh: [source,console]
$ git config --global core.whitespace \ -space-before-tab,indent-with-non-tab,tab-in-indent,cr-at-eol
Git sẽ phát hiện các vấn đề này khi bạn chạy lệnh `git diff` và cố gắng tô màu chúng để bạn có thể sửa chúng trước khi commit. Nó cũng sẽ sử dụng các giá trị này để giúp bạn khi bạn áp dụng các bản vá với `git apply`. Khi bạn đang áp dụng các bản vá, bạn có thể yêu cầu Git cảnh báo bạn nếu nó đang áp dụng các bản vá có các vấn đề về khoảng trắng được chỉ định: [source,console]
$ git apply --whitespace=warn <patch>
Hoặc bạn có thể yêu cầu Git cố gắng tự động sửa vấn đề trước khi áp dụng bản vá: [source,console]
$ git apply --whitespace=fix <patch>
Các tùy chọn này cũng áp dụng cho lệnh `git rebase`. Nếu bạn đã commit các vấn đề về khoảng trắng nhưng chưa đẩy lên thượng nguồn (upstream), bạn có thể chạy `git rebase --whitespace=fix` để yêu cầu Git tự động sửa các vấn đề về khoảng trắng khi nó viết lại các bản vá. ==== Cấu hình Máy chủ Không có nhiều tùy chọn cấu hình có sẵn cho phía máy chủ của Git, nhưng có một vài cái thú vị mà bạn có thể muốn lưu ý. ===== `receive.fsckObjects` Git có khả năng đảm bảo mọi đối tượng nhận được trong quá trình đẩy vẫn khớp với tổng kiểm tra SHA-1 của nó và trỏ đến các đối tượng hợp lệ. Tuy nhiên, nó không làm điều này theo mặc định; nó là một hoạt động khá tốn kém, và có thể làm chậm hoạt động, đặc biệt là trên các kho chứa lớn hoặc các lần đẩy lớn. Nếu bạn muốn Git kiểm tra tính nhất quán của đối tượng trên mỗi lần đẩy, bạn có thể buộc nó làm như vậy bằng cách đặt `receive.fsckObjects` thành true: [source,console]
$ git config --system receive.fsckObjects true
Bây giờ, Git sẽ kiểm tra tính toàn vẹn của kho chứa của bạn trước khi mỗi lần đẩy được chấp nhận để đảm bảo các máy khách bị lỗi (hoặc độc hại) không đưa vào dữ liệu bị hỏng. ===== `receive.denyNonFastForwards` Nếu bạn rebase các commit mà bạn đã đẩy và sau đó cố gắng đẩy lại, hoặc nếu không cố gắng đẩy một commit đến một nhánh từ xa không chứa commit mà nhánh từ xa hiện đang trỏ tới, bạn sẽ bị từ chối. Đây thường là chính sách tốt; nhưng trong trường hợp rebase, bạn có thể xác định rằng bạn biết mình đang làm gì và có thể cập nhật bắt buộc nhánh từ xa bằng cờ `-f` cho lệnh đẩy của bạn. Để bảo Git từ chối các lần đẩy bắt buộc (force-pushes), hãy đặt `receive.denyNonFastForwards`: [source,console]
$ git config --system receive.denyNonFastForwards true
Cách khác bạn có thể làm điều này là thông qua các móc nhận (receive hooks) phía máy chủ, mà chúng tôi sẽ đề cập trong giây lát. Cách tiếp cận đó cho phép bạn làm những việc phức tạp hơn như từ chối các tua nhanh (non-fast-forwards) đối với một nhóm người dùng nhất định. ===== `receive.denyDeletes` Một trong những cách giải quyết chính sách `denyNonFastForwards` là người dùng xóa nhánh và sau đó đẩy nó trở lại với tham chiếu mới. Để tránh điều này, hãy đặt `receive.denyDeletes` thành true: [source,console]
$ git config --system receive.denyDeletes true
Điều này từ chối mọi việc xóa nhánh hoặc thẻ -- không người dùng nào có thể làm điều đó. Để xóa các nhánh từ xa, bạn phải xóa các tập tin tham chiếu (ref files) khỏi máy chủ một cách thủ công. Cũng có những cách thú vị hơn để làm điều này trên cơ sở từng người dùng thông qua ACL, như bạn sẽ tìm hiểu trong <<ch08-customizing-git#_an_example_git_enforced_policy>>. === Thuộc tính Git (((attributes))) Một số thiết lập này cũng có thể được chỉ định cho một đường dẫn, để Git chỉ áp dụng các thiết lập đó cho một thư mục con hoặc một tập hợp các tập tin. Các thiết lập cụ thể theo đường dẫn này được gọi là các thuộc tính Git và được đặt trong tập tin `.gitattributes` trong một trong các thư mục của bạn (thường là thư mục gốc của dự án) hoặc trong tập tin `.git/info/attributes` nếu bạn không muốn tập tin thuộc tính được commit cùng với dự án của mình. Sử dụng các thuộc tính, bạn có thể làm những việc như chỉ định các chiến lược trộn riêng biệt cho các tập tin hoặc thư mục riêng lẻ trong dự án của mình, bảo Git cách diff các tập tin không phải văn bản, hoặc yêu cầu Git lọc nội dung trước khi bạn check nó vào hoặc ra khỏi Git. Trong phần này, bạn sẽ tìm hiểu về một số thuộc tính bạn có thể đặt trên các đường dẫn trong dự án Git của mình và xem một vài ví dụ về việc sử dụng tính năng này trong thực tế. ==== Tập tin Nhị phân (((binary files))) Một thủ thuật thú vị mà bạn có thể sử dụng các thuộc tính Git là bảo Git biết tập tin nào là nhị phân (trong trường hợp nó có thể không tự tìm ra được) và đưa ra các hướng dẫn đặc biệt cho Git về cách xử lý các tập tin đó. Ví dụ, một số tập tin văn bản có thể được tạo bởi máy và không thể diff được, trong khi một số tập tin nhị phân có thể diff được. Bạn sẽ thấy cách bảo Git cái nào là cái nào. ===== Xác định Tập tin Nhị phân Một số tập tin trông giống như tập tin văn bản nhưng về mọi mặt và mục đích đều được coi là dữ liệu nhị phân. Ví dụ, các dự án Xcode trên macOS chứa một tập tin kết thúc bằng `.pbxproj`, về cơ bản là một tập dữ liệu JSON (định dạng dữ liệu JavaScript văn bản thuần túy) được IDE ghi ra đĩa, ghi lại các thiết lập xây dựng của bạn và vân vân. Mặc dù về mặt kỹ thuật nó là một tập tin văn bản (vì nó hoàn toàn là UTF-8), bạn không muốn coi nó như vậy vì nó thực sự là một cơ sở dữ liệu nhẹ – bạn không thể trộn nội dung nếu hai người thay đổi nó, và các diff thường không hữu ích. Tập tin này được dùng để máy tiêu thụ. Về bản chất, bạn muốn coi nó như một tập tin nhị phân. Để bảo Git coi tất cả các tập tin `pbxproj` là dữ liệu nhị phân, hãy thêm dòng sau vào tập tin `.gitattributes` của bạn: [source,ini]
*.pbxproj binary
Bây giờ, Git sẽ không cố gắng chuyển đổi hoặc sửa các vấn đề CRLF; nó cũng sẽ không cố gắng tính toán hoặc in một diff cho các thay đổi trong tập tin này khi bạn chạy `git show` hoặc `git diff` trên dự án của mình. ===== Diff các Tập tin Nhị phân Bạn cũng có thể sử dụng chức năng thuộc tính Git để diff các tập tin nhị phân một cách hiệu quả. Bạn làm điều này bằng cách bảo Git cách chuyển đổi dữ liệu nhị phân của bạn sang định dạng văn bản có thể so sánh được thông qua diff thông thường. Đầu tiên, bạn sẽ sử dụng kỹ thuật này để giải quyết một trong những vấn đề khó chịu nhất được biết đến với nhân loại: kiểm soát phiên bản tài liệu Microsoft Word. Nếu bạn muốn kiểm soát phiên bản tài liệu Word, bạn có thể ném chúng vào một kho chứa Git và commit thỉnh thoảng; nhưng điều đó có ích gì? Nếu bạn chạy `git diff` thông thường, bạn chỉ thấy một cái gì đó như thế này: [source,console]
$ git diff diff --git a/chapter1.docx b/chapter1.docx index 88839c4..4afcb7c 100644 Binary files a/chapter1.docx and b/chapter1.docx differ
Bạn không thể so sánh trực tiếp hai phiên bản trừ khi bạn check out chúng và quét chúng thủ công, phải không? Hóa ra bạn có thể làm điều này khá tốt bằng cách sử dụng các thuộc tính Git. Đặt dòng sau vào tập tin `.gitattributes` của bạn: [source,ini]
*.docx diff=word
Điều này bảo Git rằng bất kỳ tập tin nào khớp với mẫu này (`.docx`) nên sử dụng bộ lọc "`word`" khi bạn cố gắng xem một diff có chứa các thay đổi. Bộ lọc "`word`" là gì? Bạn phải thiết lập nó. Ở đây bạn sẽ cấu hình Git để sử dụng chương trình `docx2txt` để chuyển đổi tài liệu Word thành các tập tin văn bản có thể đọc được, mà sau đó nó sẽ diff đúng cách. Đầu tiên, bạn sẽ cần cài đặt `docx2txt`; bạn có thể tải xuống từ https://sourceforge.net/projects/docx2txt[^]. Làm theo hướng dẫn trong tập tin `INSTALL` để đặt nó ở đâu đó mà shell của bạn có thể tìm thấy. Tiếp theo, bạn sẽ viết một tập tin kịch bản bao bọc để chuyển đổi đầu ra sang định dạng mà Git mong đợi. Tạo một tập tin ở đâu đó trong đường dẫn của bạn có tên `docx2txt`, và thêm các nội dung này: [source,console]
#!/bin/bash docx2txt.pl "$1" -
Đừng quên `chmod a+x` tập tin đó. Cuối cùng, bạn có thể cấu hình Git để sử dụng tập tin kịch bản này: [source,console]
$ git config diff.word.textconv docx2txt
Bây giờ Git biết rằng nếu nó cố gắng thực hiện một diff giữa hai ảnh chụp nhanh (snapshots), và bất kỳ tập tin nào kết thúc bằng `.docx`, nó nên chạy các tập tin đó qua bộ lọc "`word`", được định nghĩa là chương trình `docx2txt`. Điều này thực sự tạo ra các phiên bản dựa trên văn bản đẹp của các tập tin Word của bạn trước khi cố gắng diff chúng. Đây là một ví dụ: Chương 1 của cuốn sách này đã được chuyển đổi sang định dạng Word và được commit trong một kho chứa Git. Sau đó, một đoạn văn mới đã được thêm vào. Đây là những gì `git diff` hiển thị: [source,console]
$ git diff diff --git a/chapter1.docx b/chapter1.docx index 0b013ca..ba25db5 100644 --- a/chapter1.docx + b/chapter1.docx @@ -2,6 +2,7 @@ This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so. 1.1. About Version Control What is "version control", and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. For the examples in this book you will use software source code as the files being version controlled, though in reality you can do this with nearly any type of file on a computer. +Testing: 1, 2, 3. If you are a graphic or web designer and want to keep every version of an image or layout (which you would most certainly want to), a Version Control System (VCS) is a very wise thing to use. It allows you to revert files back to a previous state, revert the entire project back to a previous state, compare changes over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also generally means that if you screw things up or lose files, you can easily recover. In addition, you get all this for very little overhead. 1.1.1. Local Version Control Systems Many people’s version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if they’re clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory you’re in and accidentally write to the wrong file or copy over files you don’t mean to.
Git thành công và ngắn gọn cho chúng ta biết rằng chúng ta đã thêm chuỗi "`Testing: 1, 2, 3.`", điều này là chính xác. Nó không hoàn hảo – các thay đổi định dạng sẽ không hiển thị ở đây – nhưng nó chắc chắn hoạt động. Một vấn đề thú vị khác mà bạn có thể giải quyết theo cách này liên quan đến việc diff các tập tin hình ảnh. Một cách để làm điều này là chạy các tập tin hình ảnh qua một bộ lọc trích xuất thông tin EXIF của chúng – siêu dữ liệu được ghi lại với hầu hết các định dạng hình ảnh. Nếu bạn tải xuống và cài đặt chương trình `exiftool`, bạn có thể sử dụng nó để chuyển đổi hình ảnh của mình thành văn bản về siêu dữ liệu, vì vậy ít nhất diff sẽ hiển thị cho bạn một biểu diễn văn bản của bất kỳ thay đổi nào đã xảy ra. Đặt dòng sau vào tập tin `.gitattributes` của bạn: [source,ini]
*.png diff=exif
Cấu hình Git để sử dụng công cụ này: [source,console]
$ git config diff.exif.textconv exiftool
Nếu bạn thay thế một hình ảnh trong dự án của mình và chạy `git diff`, bạn thấy một cái gì đó như thế này: [source,diff]
diff --git a/image.png b/image.png index 88839c4..4afcb7c 100644 --- a/image.png + b/image.png @@ -1,12 +1,12 @@ ExifTool Version Number : 7.74 -File Size : 70 kB -File Modification Date/Time : 2009:04:21 07:02:45-07:00 +File Size : 94 kB +File Modification Date/Time : 2009:04:21 07:02:43-07:00 File Type : PNG MIME Type : image/png -Image Width : 1058 -Image Height : 889 +Image Width : 1056 +Image Height : 827 Bit Depth : 8 Color Type : RGB with Alpha
Bạn có thể dễ dàng thấy rằng kích thước tập tin và kích thước hình ảnh đều đã thay đổi. [[_keyword_expansion]] ==== Mở rộng Từ khóa (((keyword expansion))) Mở rộng từ khóa kiểu SVN- hoặc CVS- thường được yêu cầu bởi các nhà phát triển đã quen với các hệ thống đó. Vấn đề chính với điều này trong Git là bạn không thể sửa đổi một tập tin với thông tin về commit sau khi bạn đã commit, bởi vì Git kiểm tra tổng (checksums) tập tin trước. Tuy nhiên, bạn có thể tiêm văn bản vào một tập tin khi nó được check out và xóa nó một lần nữa trước khi nó được thêm vào một commit. Các thuộc tính Git cung cấp cho bạn hai cách để làm điều này. Đầu tiên, bạn có thể tiêm tổng kiểm tra SHA-1 của một blob vào một trường `$Id$` trong tập tin một cách tự động. Nếu bạn đặt thuộc tính này trên một tập tin hoặc tập hợp các tập tin, thì lần tới khi bạn check out nhánh đó, Git sẽ thay thế trường đó bằng SHA-1 của blob. Điều quan trọng cần lưu ý là nó không phải là SHA-1 của commit, mà là của chính blob đó. Đặt dòng sau vào tập tin `.gitattributes` của bạn: [source,ini]
*.txt ident
Thêm một tham chiếu `$Id$` vào một tập tin thử nghiệm: [source,console]
$ echo '$Id$' > test.txt
Lần tới khi bạn check out tập tin này, Git tiêm SHA-1 của blob: [source,console]
$ rm test.txt $ git checkout — test.txt $ cat test.txt $Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $
Tuy nhiên, kết quả đó có công dụng hạn chế.
Nếu bạn đã sử dụng thay thế từ khóa trong CVS hoặc Subversion, bạn có thể bao gồm một dấu thời gian (datestamp) – SHA-1 không hữu ích lắm, bởi vì nó khá ngẫu nhiên và bạn không thể biết liệu một SHA-1 cũ hơn hay mới hơn cái khác chỉ bằng cách nhìn vào chúng.
Hóa ra bạn có thể viết các bộ lọc của riêng mình để thực hiện thay thế trong các tập tin khi commit/checkout.
Chúng được gọi là các bộ lọc "`clean`" (làm sạch) và "`smudge`" (làm nhòe).
Trong tập tin `.gitattributes`, bạn có thể đặt một bộ lọc cho các đường dẫn cụ thể và sau đó thiết lập các tập tin kịch bản sẽ xử lý các tập tin ngay trước khi chúng được check out ("`smudge`", xem <<filters_a>>) và ngay trước khi chúng được stage ("`clean`", xem <<filters_b>>).
Các bộ lọc này có thể được thiết lập để làm tất cả các loại điều thú vị.
[[filters_a]]
.Bộ lọc "`smudge`" được chạy khi checkout
image::images/smudge.png[Bộ lọc “smudge” được chạy khi checkout]
[[filters_b]]
.Bộ lọc "`clean`" được chạy khi các tập tin được stage
image::images/clean.png[Bộ lọc “clean” được chạy khi các tập tin được stage]
Thông điệp commit ban đầu cho tính năng này đưa ra một ví dụ đơn giản về việc chạy tất cả mã nguồn C của bạn thông qua chương trình `indent` trước khi commit.
Bạn có thể thiết lập nó bằng cách đặt thuộc tính bộ lọc trong tập tin `.gitattributes` của bạn để lọc các tập tin `\*.c` với bộ lọc "`indent`":
[source,ini]
*.c filter=indent
Sau đó, bảo Git bộ lọc "`indent`" làm gì khi smudge và clean: [source,console]
$ git config --global filter.indent.clean indent $ git config --global filter.indent.smudge cat
Trong trường hợp này, khi bạn commit các tập tin khớp với `*.c`, Git sẽ chạy chúng qua chương trình indent trước khi nó stage chúng và sau đó chạy chúng qua chương trình `cat` trước khi nó check chúng trở lại đĩa. Chương trình `cat` về cơ bản không làm gì cả: nó nhả ra cùng một dữ liệu mà nó nhận vào. Sự kết hợp này lọc hiệu quả tất cả các tập tin mã nguồn C qua `indent` trước khi commit. Một ví dụ thú vị khác nhận được mở rộng từ khóa `$Date$`, kiểu RCS. Để làm điều này đúng cách, bạn cần một tập tin kịch bản nhỏ nhận tên tập tin, tìm ra ngày commit cuối cùng cho dự án này, và chèn ngày vào tập tin. Đây là một tập tin kịch bản Ruby nhỏ làm điều đó: [source,ruby]
#! /usr/bin/env ruby
data = STDIN.read
last_date = git log --pretty=format:"%ad" -1
puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')
Tất cả những gì tập tin kịch bản làm là lấy ngày commit mới nhất từ lệnh `git log`, dán nó vào bất kỳ chuỗi `$Date$` nào nó thấy trong stdin, và in kết quả – nó sẽ đơn giản để làm trong bất kỳ ngôn ngữ nào bạn thoải mái nhất. Bạn có thể đặt tên tập tin này là `expand_date` và đặt nó trong đường dẫn của bạn. Bây giờ, bạn cần thiết lập một bộ lọc trong Git (gọi nó là `dater`) và bảo nó sử dụng bộ lọc `expand_date` của bạn để smudge các tập tin khi checkout. Bạn sẽ sử dụng một biểu thức Perl để làm sạch (clean) nó khi commit: [source,console]
$ git config filter.dater.smudge expand_date $ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'
Đoạn mã Perl này loại bỏ bất cứ thứ gì nó thấy trong một chuỗi `$Date$`, để quay lại nơi bạn bắt đầu. Bây giờ bộ lọc của bạn đã sẵn sàng, bạn có thể kiểm tra nó bằng cách thiết lập một thuộc tính Git cho tập tin đó tham gia vào bộ lọc mới và tạo một tập tin với từ khóa `$Date$` của bạn: [source,ini]
date*.txt filter=dater
[source,console]
$ echo '# $Date$' > date_test.txt
Nếu bạn commit những thay đổi đó và check out tập tin một lần nữa, bạn thấy từ khóa được thay thế đúng cách: [source,console]
$ git add date_test.txt .gitattributes $ git commit -m "Test date expansion in Git" $ rm date_test.txt $ git checkout date_test.txt $ cat date_test.txt # $Date: Tue Apr 21 07:26:52 2009 -0700$
Bạn có thể thấy kỹ thuật này có thể mạnh mẽ như thế nào cho các ứng dụng tùy chỉnh. Tuy nhiên, bạn phải cẩn thận, bởi vì tập tin `.gitattributes` được commit và truyền đi cùng với dự án, nhưng trình điều khiển (trong trường hợp này, `dater`) thì không, vì vậy nó sẽ không hoạt động ở mọi nơi. Khi bạn thiết kế các bộ lọc này, chúng nên có thể thất bại một cách duyên dáng (fail gracefully) và để dự án vẫn hoạt động bình thường. ==== Xuất Kho chứa của Bạn (((archiving))) Dữ liệu thuộc tính Git cũng cho phép bạn làm một số điều thú vị khi xuất một bản lưu trữ (archive) của dự án của bạn. ===== `export-ignore` Bạn có thể bảo Git không xuất một số tập tin hoặc thư mục nhất định khi tạo một bản lưu trữ. Nếu có một thư mục con hoặc tập tin mà bạn không muốn bao gồm trong tập tin lưu trữ của mình nhưng bạn muốn check vào dự án của mình, bạn có thể xác định các tập tin đó thông qua thuộc tính `export-ignore`. Ví dụ, giả sử bạn có một số tập tin kiểm tra trong thư mục con `test/`, và việc bao gồm chúng trong bản xuất tarball của dự án của bạn là không hợp lý. Bạn có thể thêm dòng sau vào tập tin thuộc tính Git của mình: [source,ini]
test/ export-ignore
Bây giờ, khi bạn chạy `git archive` để tạo một tarball của dự án của bạn, thư mục đó sẽ không được bao gồm trong bản lưu trữ. ===== `export-subst` Khi xuất các tập tin để triển khai, bạn có thể áp dụng định dạng và xử lý mở rộng từ khóa của ``git log`` cho các phần được chọn của các tập tin được đánh dấu bằng thuộc tính ``export-subst``. Ví dụ, nếu bạn muốn bao gồm một tập tin có tên `LAST_COMMIT` trong dự án của mình, và có siêu dữ liệu về commit cuối cùng được tự động tiêm vào nó khi `git archive` chạy, bạn có thể ví dụ thiết lập các tập tin `.gitattributes` và `LAST_COMMIT` của mình như thế này: [source,ini]
LAST_COMMIT export-subst
[source,console]
$ echo 'Last commit date: $Format:%cd by %aN$' > LAST_COMMIT $ git add LAST_COMMIT .gitattributes $ git commit -am 'adding LAST_COMMIT file for archives'
Khi bạn chạy `git archive`, nội dung của tập tin được lưu trữ sẽ trông giống như thế này: [source,console]
$ git archive HEAD | tar xCf ../deployment-testing - $ cat ../deployment-testing/LAST_COMMIT Last commit date: Tue Apr 21 08:38:48 2009 -0700 by Scott Chacon
Các thay thế có thể bao gồm ví dụ thông điệp commit và bất kỳ `git notes` nào, và `git log` có thể thực hiện ngắt dòng đơn giản: [source,console]
$ echo '$Format:Last commit: %h by %aN at %cd%n%+w(76,6,9)%B$' > LAST_COMMIT $ git commit -am 'export-subst uses git log'\''s custom formatter
git archive uses git log'\''s pretty=format: processor
directly, and strips the surrounding $Format: and $
markup from the output.
'
$ git archive @ | tar xfO - LAST_COMMIT
Last commit: 312ccc8 by Jim Hill at Fri May 8 09:14:04 2015 -0700
export-subst uses git log’s custom formatter
git archive uses git log's `pretty=format:` processor directly, and strips the surrounding `$Format:` and `$` markup from the output.
Bản lưu trữ kết quả phù hợp cho công việc triển khai, nhưng giống như bất kỳ bản lưu trữ được xuất nào, nó không phù hợp cho công việc phát triển tiếp theo. ==== Chiến lược Trộn (((merging, strategies))) Bạn cũng có thể sử dụng các thuộc tính Git để bảo Git sử dụng các chiến lược trộn khác nhau cho các tập tin cụ thể trong dự án của bạn. Một tùy chọn rất hữu ích là bảo Git không cố gắng trộn các tập tin cụ thể khi chúng có xung đột, mà thay vào đó sử dụng phía của bạn trong việc trộn thay vì của người khác. Điều này hữu ích nếu một nhánh trong dự án của bạn đã phân kỳ hoặc được chuyên biệt hóa, nhưng bạn muốn có thể trộn các thay đổi trở lại từ nó, và bạn muốn bỏ qua một số tập tin nhất định. Giả sử bạn có một tập tin thiết lập cơ sở dữ liệu gọi là `database.xml` khác nhau trong hai nhánh, và bạn muốn trộn vào nhánh khác của mình mà không làm hỏng tập tin cơ sở dữ liệu. Bạn có thể thiết lập một thuộc tính như thế này: [source,ini]
database.xml merge=ours
Và sau đó định nghĩa một chiến lược trộn `ours` giả với: [source,console]
$ git config --global merge.ours.driver true
Nếu bạn trộn vào nhánh khác, thay vì có xung đột trộn với tập tin `database.xml`, bạn thấy một cái gì đó như thế này: [source,console]
$ git merge topic Auto-merging database.xml Merge made by recursive.
Trong trường hợp này, `database.xml` giữ nguyên ở bất kỳ phiên bản nào bạn có ban đầu. [[_git_hooks]] === Móc Git (Git Hooks) (((hooks))) Giống như nhiều Hệ thống Kiểm soát Phiên bản khác, Git có một cách để kích hoạt các tập tin kịch bản tùy chỉnh khi các hành động quan trọng nhất định xảy ra. Có hai nhóm móc này: phía máy khách và phía máy chủ. Các móc phía máy khách được kích hoạt bởi các hoạt động như commit và trộn, trong khi các móc phía máy chủ chạy trên các hoạt động mạng như nhận các commit được đẩy. Bạn có thể sử dụng các móc này cho đủ loại lý do. ==== Cài đặt một Móc Các móc đều được lưu trữ trong thư mục con `hooks` của thư mục Git. Trong hầu hết các dự án, đó là `.git/hooks`. Khi bạn khởi tạo một kho chứa mới với `git init`, Git điền vào thư mục hooks một loạt các tập tin kịch bản ví dụ, nhiều trong số đó tự bản thân chúng đã hữu ích; nhưng chúng cũng ghi lại các giá trị đầu vào của mỗi tập tin kịch bản. Tất cả các ví dụ đều được viết dưới dạng tập tin kịch bản shell, với một chút Perl được thêm vào, nhưng bất kỳ tập tin kịch bản thực thi nào được đặt tên đúng cách sẽ hoạt động tốt – bạn có thể viết chúng bằng Ruby hoặc Python hoặc bất kỳ ngôn ngữ nào bạn quen thuộc. Nếu bạn muốn sử dụng các tập tin kịch bản móc đi kèm, bạn sẽ phải đổi tên chúng; tên tập tin của chúng đều kết thúc bằng `.sample`. Để kích hoạt một tập tin kịch bản móc, hãy đặt một tập tin trong thư mục con `hooks` của thư mục `.git` của bạn được đặt tên thích hợp (không có bất kỳ phần mở rộng nào) và có thể thực thi được. Từ thời điểm đó trở đi, nó sẽ được gọi. Chúng tôi sẽ đề cập đến hầu hết các tên tập tin móc chính ở đây. ==== Móc Phía Máy Khách Có rất nhiều móc phía máy khách. Phần này chia chúng thành các móc quy trình commit, các tập tin kịch bản quy trình email, và mọi thứ khác. [NOTE] ==== Điều quan trọng cần lưu ý là các móc phía máy khách *không* được sao chép khi bạn sao chép (clone) một kho chứa. Nếu ý định của bạn với các tập tin kịch bản này là thực thi một chính sách, bạn có thể sẽ muốn làm điều đó ở phía máy chủ; xem ví dụ trong <<ch08-customizing-git#_an_example_git_enforced_policy>>. ==== ===== Móc Quy trình Commit Bốn móc đầu tiên liên quan đến quy trình commit. Móc `pre-commit` được chạy đầu tiên, trước khi bạn thậm chí gõ vào một thông điệp commit. Nó được sử dụng để kiểm tra ảnh chụp nhanh sắp được commit, để xem bạn có quên gì không, để đảm bảo các bài kiểm tra chạy, hoặc để kiểm tra bất cứ điều gì bạn cần kiểm tra trong mã. Thoát với mã khác không từ móc này sẽ hủy bỏ commit, mặc dù bạn có thể bỏ qua nó với `git commit --no-verify`. Bạn có thể làm những việc như kiểm tra phong cách mã (chạy `lint` hoặc thứ gì đó tương đương), kiểm tra khoảng trắng ở cuối dòng (móc mặc định làm chính xác điều này), hoặc kiểm tra tài liệu thích hợp trên các phương thức mới. Móc `prepare-commit-msg` được chạy trước khi trình soạn thảo thông điệp commit được kích hoạt nhưng sau khi thông điệp mặc định được tạo. Nó cho phép bạn chỉnh sửa thông điệp mặc định trước khi tác giả commit nhìn thấy nó. Móc này nhận một vài tham số: đường dẫn đến tập tin chứa thông điệp commit cho đến nay, loại commit, và SHA-1 commit nếu đây là một commit sửa đổi (amended commit). Móc này thường không hữu ích cho các commit bình thường; thay vào đó, nó tốt cho các commit mà thông điệp mặc định được tạo tự động, chẳng hạn như các thông điệp commit theo mẫu, các commit trộn, các commit squash, và các commit sửa đổi. Bạn có thể sử dụng nó kết hợp với một mẫu commit để chèn thông tin theo chương trình. Móc `commit-msg` nhận một tham số, một lần nữa là đường dẫn đến một tập tin tạm thời chứa thông điệp commit được viết bởi nhà phát triển. Nếu tập tin kịch bản này thoát với mã khác không, Git sẽ hủy bỏ quy trình commit, vì vậy bạn có thể sử dụng nó để xác thực trạng thái dự án hoặc thông điệp commit của mình trước khi cho phép một commit đi qua. Trong phần cuối của chương này, chúng tôi sẽ minh họa việc sử dụng móc này để kiểm tra xem thông điệp commit của bạn có tuân thủ một mẫu bắt buộc hay không. Sau khi toàn bộ quy trình commit hoàn tất, móc `post-commit` chạy. Nó không nhận bất kỳ tham số nào, nhưng bạn có thể dễ dàng lấy commit cuối cùng bằng cách chạy `git log -1 HEAD`. Nói chung, tập tin kịch bản này được sử dụng để thông báo hoặc một cái gì đó tương tự. [[_email_hooks]] ===== Móc Quy trình Email Bạn có thể thiết lập ba móc phía máy khách cho một quy trình làm việc dựa trên email. Tất cả chúng đều được gọi bởi lệnh `git am`, vì vậy nếu bạn không sử dụng lệnh đó trong quy trình làm việc của mình, bạn có thể bỏ qua phần tiếp theo một cách an toàn. Nếu bạn đang nhận các bản vá qua email được chuẩn bị bởi `git format-patch`, thì một số trong số này có thể hữu ích cho bạn. Móc đầu tiên được chạy là `applypatch-msg`. Nó nhận một đối số duy nhất: tên của tập tin tạm thời chứa thông điệp commit được đề xuất. Git hủy bỏ bản vá nếu tập tin kịch bản này thoát với mã khác không. Bạn có thể sử dụng điều này để đảm bảo một thông điệp commit được định dạng đúng, hoặc để chuẩn hóa thông điệp bằng cách cho tập tin kịch bản chỉnh sửa nó tại chỗ. Móc tiếp theo chạy khi áp dụng các bản vá thông qua `git am` là `pre-applypatch`. Hơi khó hiểu, nó được chạy _sau khi_ bản vá được áp dụng nhưng trước khi một commit được thực hiện, vì vậy bạn có thể sử dụng nó để kiểm tra ảnh chụp nhanh trước khi thực hiện commit. Bạn có thể chạy các bài kiểm tra hoặc kiểm tra cây làm việc với tập tin kịch bản này. Nếu thiếu thứ gì đó hoặc các bài kiểm tra không vượt qua, thoát với mã khác không sẽ hủy bỏ tập tin kịch bản `git am` mà không commit bản vá. Móc cuối cùng chạy trong một hoạt động `git am` là `post-applypatch`, chạy sau khi commit được thực hiện. Bạn có thể sử dụng nó để thông báo cho một nhóm hoặc tác giả của bản vá mà bạn đã kéo vào rằng bạn đã làm như vậy. Bạn không thể dừng quy trình vá với tập tin kịch bản này. [[_other_client_hooks]] ===== Các Móc Máy Khách Khác Móc `pre-rebase` chạy trước khi bạn rebase bất cứ thứ gì và có thể dừng quy trình bằng cách thoát với mã khác không. Bạn có thể sử dụng móc này để không cho phép rebase bất kỳ commit nào đã được đẩy. Móc `pre-rebase` ví dụ mà Git cài đặt làm điều này, mặc dù nó đưa ra một số giả định có thể không khớp với quy trình làm việc của bạn. Móc `post-rewrite` được chạy bởi các lệnh thay thế các commit, chẳng hạn như `git commit --amend` và `git rebase` (mặc dù không phải bởi `git filter-branch`). Đối số duy nhất của nó là lệnh nào đã kích hoạt việc viết lại, và nó nhận một danh sách các lần viết lại trên `stdin`. Móc này có nhiều công dụng giống như các móc `post-checkout` và `post-merge`. Sau khi bạn chạy một `git checkout` thành công, móc `post-checkout` chạy; bạn có thể sử dụng nó để thiết lập thư mục làm việc của mình đúng cách cho môi trường dự án của bạn. Điều này có thể có nghĩa là di chuyển vào các tập tin nhị phân lớn mà bạn không muốn kiểm soát nguồn, tự động tạo tài liệu, hoặc một cái gì đó tương tự. Móc `post-merge` chạy sau một lệnh `merge` thành công. Bạn có thể sử dụng nó để khôi phục dữ liệu trong cây làm việc mà Git không thể theo dõi, chẳng hạn như dữ liệu quyền. Móc này cũng có thể xác thực sự hiện diện của các tập tin bên ngoài sự kiểm soát của Git mà bạn có thể muốn sao chép vào khi cây làm việc thay đổi. Móc `pre-push` chạy trong quá trình `git push`, sau khi các tham chiếu từ xa đã được cập nhật nhưng trước khi bất kỳ đối tượng nào được chuyển đi. Nó nhận tên và vị trí của máy chủ từ xa làm tham số, và một danh sách các tham chiếu sắp được cập nhật thông qua `stdin`. Bạn có thể sử dụng nó để xác thực một tập hợp các cập nhật tham chiếu trước khi một lần đẩy xảy ra (mã thoát khác không sẽ hủy bỏ lần đẩy). Git thỉnh thoảng thực hiện thu gom rác như một phần của hoạt động bình thường của nó, bằng cách gọi `git gc --auto`. Móc `pre-auto-gc` được gọi ngay trước khi việc thu gom rác diễn ra, và có thể được sử dụng để thông báo cho bạn rằng điều này đang xảy ra, hoặc để hủy bỏ việc thu gom nếu bây giờ không phải là thời điểm tốt. ==== Móc Phía Máy Chủ Ngoài các móc phía máy khách, bạn có thể sử dụng một vài móc phía máy chủ quan trọng với tư cách là quản trị viên hệ thống để thực thi gần như bất kỳ loại chính sách nào cho dự án của bạn. Các tập tin kịch bản này chạy trước và sau các lần đẩy đến máy chủ. Các móc pre (trước) có thể thoát với mã khác không bất cứ lúc nào để từ chối lần đẩy cũng như in một thông báo lỗi lại cho máy khách; bạn có thể thiết lập một chính sách đẩy phức tạp như bạn muốn. ===== `pre-receive` Tập tin kịch bản đầu tiên chạy khi xử lý một lần đẩy từ máy khách là `pre-receive`. Nó nhận một danh sách các tham chiếu đang được đẩy từ stdin; nếu nó thoát với mã khác không, không có cái nào được chấp nhận. Bạn có thể sử dụng móc này để làm những việc như đảm bảo không có tham chiếu cập nhật nào là không tua nhanh (non-fast-forwards), hoặc để kiểm soát truy cập cho tất cả các tham chiếu và tập tin họ đang sửa đổi với lần đẩy. ===== `update` Tập tin kịch bản `update` rất giống với tập tin kịch bản `pre-receive`, ngoại trừ việc nó được chạy một lần cho mỗi nhánh mà người đẩy đang cố gắng cập nhật. Nếu người đẩy đang cố gắng đẩy đến nhiều nhánh, `pre-receive` chỉ chạy một lần, trong khi `update` chạy một lần cho mỗi nhánh họ đang đẩy đến. Thay vì đọc từ stdin, tập tin kịch bản này nhận ba đối số: tên của tham chiếu (nhánh), SHA-1 mà tham chiếu trỏ đến trước khi đẩy, và SHA-1 mà người dùng đang cố gắng đẩy. Nếu tập tin kịch bản `update` thoát với mã khác không, chỉ tham chiếu đó bị từ chối; các tham chiếu khác vẫn có thể được cập nhật. ===== `post-receive` Móc `post-receive` chạy sau khi toàn bộ quy trình hoàn tất và có thể được sử dụng để cập nhật các dịch vụ khác hoặc thông báo cho người dùng. Nó nhận cùng dữ liệu stdin như móc `pre-receive`. Các ví dụ bao gồm gửi email cho một danh sách, thông báo cho máy chủ tích hợp liên tục, hoặc cập nhật hệ thống theo dõi vé – bạn thậm chí có thể phân tích cú pháp các thông điệp commit để xem có vé nào cần được mở, sửa đổi hoặc đóng hay không. Tập tin kịch bản này không thể dừng quy trình đẩy, nhưng máy khách không ngắt kết nối cho đến khi nó hoàn tất, vì vậy hãy cẩn thận nếu bạn cố gắng làm bất cứ điều gì có thể mất nhiều thời gian. [TIP] ==== Nếu bạn đang viết một tập tin kịch bản/móc mà người khác sẽ cần đọc, hãy ưu tiên các phiên bản dài của cờ dòng lệnh; sáu tháng sau bạn sẽ cảm ơn chúng tôi. ==== [[_an_example_git_enforced_policy]] === Một Ví dụ về Chính sách được Thực thi bởi Git (((policy example))) Trong phần này, bạn sẽ sử dụng những gì bạn đã học để thiết lập một quy trình làm việc Git kiểm tra định dạng thông điệp commit tùy chỉnh, và chỉ cho phép một số người dùng nhất định sửa đổi các thư mục con nhất định trong một dự án. Bạn sẽ xây dựng các tập tin kịch bản phía máy khách giúp nhà phát triển biết liệu lần đẩy của họ có bị từ chối hay không và các tập tin kịch bản phía máy chủ thực sự thực thi các chính sách. Các tập tin kịch bản chúng tôi sẽ hiển thị được viết bằng Ruby; một phần vì quán tính trí tuệ của chúng tôi, nhưng cũng vì Ruby dễ đọc, ngay cả khi bạn không nhất thiết phải viết được nó. Tuy nhiên, bất kỳ ngôn ngữ nào cũng sẽ hoạt động – tất cả các tập tin kịch bản móc mẫu được phân phối với Git đều bằng Perl hoặc Bash, vì vậy bạn cũng có thể xem nhiều ví dụ về các móc trong các ngôn ngữ đó bằng cách xem các mẫu. ==== Móc Phía Máy Chủ Tất cả công việc phía máy chủ sẽ đi vào tập tin `update` trong thư mục `hooks` của bạn. Móc `update` chạy một lần cho mỗi nhánh đang được đẩy và nhận ba đối số: * Tên của tham chiếu đang được đẩy đến * Bản sửa đổi cũ nơi nhánh đó đã ở * Bản sửa đổi mới đang được đẩy Bạn cũng có quyền truy cập vào người dùng đang thực hiện việc đẩy nếu việc đẩy đang được chạy qua SSH. Nếu bạn đã cho phép mọi người kết nối với một người dùng duy nhất (như "`git`") thông qua xác thực khóa công khai, bạn có thể phải cung cấp cho người dùng đó một trình bao bọc shell xác định người dùng nào đang kết nối dựa trên khóa công khai, và thiết lập một biến môi trường cho phù hợp. Ở đây chúng tôi sẽ giả định người dùng đang kết nối nằm trong biến môi trường `$USER`, vì vậy tập tin kịch bản cập nhật của bạn bắt đầu bằng cách thu thập tất cả thông tin bạn cần: [source,ruby]
#!/usr/bin/env ruby
$refname = ARGV[0] $oldrev = ARGV[1] $newrev = ARGV[2] $user = ENV['USER']
puts "Enforcing Policies…" puts "({$refname}) ({$oldrev[0,6]}) (#{$newrev[0,6]})"
Vâng, đó là các biến toàn cục. Đừng phán xét – cách này dễ minh họa hơn. [[_enforcing_commit_message_format]] ===== Thực thi một Định dạng Thông điệp Commit Cụ thể Thách thức đầu tiên của bạn là thực thi việc mỗi thông điệp commit tuân thủ một định dạng cụ thể. Chỉ để có một mục tiêu, giả sử rằng mỗi thông điệp phải bao gồm một chuỗi trông giống như "`ref: 1234`" bởi vì bạn muốn mỗi commit liên kết đến một mục công việc trong hệ thống bán vé của bạn. Bạn phải xem xét từng commit đang được đẩy lên, xem chuỗi đó có trong thông điệp commit hay không, và, nếu chuỗi đó vắng mặt trong bất kỳ commit nào, hãy thoát với mã khác không để lần đẩy bị từ chối. Bạn có thể lấy danh sách các giá trị SHA-1 của tất cả các commit đang được đẩy bằng cách lấy các giá trị `$newrev` và `$oldrev` và chuyển chúng cho một lệnh plumbing của Git gọi là `git rev-list`. Về cơ bản đây là lệnh `git log`, nhưng theo mặc định nó chỉ in ra các giá trị SHA-1 và không có thông tin nào khác. Vì vậy, để lấy danh sách tất cả các SHA-1 commit được giới thiệu giữa một SHA-1 commit này và một cái khác, bạn có thể chạy một cái gì đó như thế này: [source,console]
$ git rev-list 538c33..d14fc7 d14fc7c847ab946ec39590d87783c69b031bdfb7 9f585da4401b0a3999e84113824d15245c13f0be 234071a1be950e2a8d078e6141f5cd20c1e61ad3 dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a 17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
Bạn có thể lấy đầu ra đó, lặp qua từng SHA-1 commit đó, lấy thông điệp cho nó, và kiểm tra thông điệp đó so với một biểu thức chính quy tìm kiếm một mẫu. Bạn phải tìm ra cách lấy thông điệp commit từ mỗi commit này để kiểm tra. Để lấy dữ liệu commit thô, bạn có thể sử dụng một lệnh plumbing khác gọi là `git cat-file`. Chúng tôi sẽ xem xét tất cả các lệnh plumbing này một cách chi tiết trong <<ch10-git-internals#ch10-git-internals>>; nhưng hiện tại, đây là những gì lệnh đó cung cấp cho bạn: [source,console]
$ git cat-file commit ca82a6 tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 author Scott Chacon <schacon@gmail.com> 1205815931 -0700 committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
Change the version number
Một cách đơn giản để lấy thông điệp commit từ một commit khi bạn có giá trị SHA-1 là đi đến dòng trống đầu tiên và lấy mọi thứ sau đó. Bạn có thể làm như vậy với lệnh `sed` trên các hệ thống Unix: [source,console]
$ git cat-file commit ca82a6 | sed '1,/^$/d' Change the version number
Bạn có thể sử dụng câu thần chú đó để lấy thông điệp commit từ mỗi commit đang cố gắng được đẩy và thoát nếu bạn thấy bất cứ điều gì không khớp. Để thoát khỏi tập tin kịch bản và từ chối lần đẩy, hãy thoát với mã khác không. Toàn bộ phương pháp trông giống như thế này: [source,ruby]
$regex = /\[ref: (\d+)\]/
# enforced custom commit message format
def check_message_format
missed_revs = git rev-list {$oldrev}..{$newrev}.split("\n")
missed_revs.each do |rev|
message = git cat-file commit #{rev} | sed '1,/^$/d'
if !$regex.match(message)
puts "[POLICY] Your message is not formatted correctly"
exit 1
end
end
end
check_message_format
Đặt cái đó vào tập tin kịch bản `update` của bạn sẽ từ chối các bản cập nhật chứa các commit có thông điệp không tuân thủ quy tắc của bạn. ===== Thực thi Hệ thống ACL Dựa trên Người dùng Giả sử bạn muốn thêm một cơ chế sử dụng danh sách kiểm soát truy cập (ACL) chỉ định người dùng nào được phép đẩy các thay đổi đến phần nào của dự án của bạn. Một số người có quyền truy cập đầy đủ, và những người khác chỉ có thể đẩy các thay đổi đến các thư mục con nhất định hoặc các tập tin cụ thể. Để thực thi điều này, bạn sẽ viết các quy tắc đó vào một tập tin có tên `acl` nằm trong kho chứa Git bare của bạn trên máy chủ. Bạn sẽ để móc `update` xem xét các quy tắc đó, xem những tập tin nào đang được giới thiệu cho tất cả các commit đang được đẩy, và xác định xem người dùng thực hiện việc đẩy có quyền cập nhật tất cả các tập tin đó hay không. Điều đầu tiên bạn sẽ làm là viết ACL của mình. Ở đây bạn sẽ sử dụng một định dạng rất giống với cơ chế ACL của CVS: nó sử dụng một loạt các dòng, trong đó trường đầu tiên là `avail` hoặc `unavail`, trường tiếp theo là danh sách người dùng được phân tách bằng dấu phẩy mà quy tắc áp dụng, và trường cuối cùng là đường dẫn mà quy tắc áp dụng (trống có nghĩa là truy cập mở). Tất cả các trường này được phân tách bằng ký tự ống (`|`). Trong trường hợp này, bạn có một vài quản trị viên, một số người viết tài liệu có quyền truy cập vào thư mục `doc`, và một nhà phát triển chỉ có quyền truy cập vào các thư mục `lib` và `tests`, và tập tin ACL của bạn trông giống như thế này: [source]
avail|nickh,pjhyett,defunkt,tpw avail|usinclair,cdickens,ebronte|doc avail|schacon|lib avail|schacon|tests
Bạn bắt đầu bằng cách đọc dữ liệu này vào một cấu trúc mà bạn có thể sử dụng. Trong trường hợp này, để giữ cho ví dụ đơn giản, bạn sẽ chỉ thực thi các chỉ thị `avail`. Đây là một phương thức cung cấp cho bạn một mảng liên kết trong đó khóa là tên người dùng và giá trị là một mảng các đường dẫn mà người dùng có quyền ghi: [source,ruby]
def get_acl_access_data(acl_file) # read in ACL data acl_file = File.read(acl_file).split("\n").reject { |line| line == '' } access = {} acl_file.each do |line| avail, users, path = line.split('|') next unless avail == 'avail' users.split(',').each do |user| access[user] ||= [] access[user] << path end end access end
Trên tập tin ACL bạn đã xem trước đó, phương thức `get_acl_access_data` này trả về một cấu trúc dữ liệu trông giống như thế này: [source,ruby]
{"defunkt"⇒[nil], "tpw"⇒[nil], "nickh"⇒[nil], "pjhyett"⇒[nil], "schacon"⇒["lib", "tests"], "cdickens"⇒["doc"], "usinclair"⇒["doc"], "ebronte"⇒["doc"]}
Bây giờ bạn đã sắp xếp xong các quyền, bạn cần xác định những đường dẫn nào mà các commit đang được đẩy đã sửa đổi, để bạn có thể đảm bảo người dùng đang đẩy có quyền truy cập vào tất cả chúng. Bạn có thể thấy khá dễ dàng những tập tin nào đã được sửa đổi trong một commit duy nhất với tùy chọn `--name-only` cho lệnh `git log` (được đề cập ngắn gọn trong <<ch02-git-basics-chapter#ch02-git-basics-chapter>>): [source,console]
$ git log -1 --name-only --pretty=format:'' 9f585d
README lib/test.rb
Nếu bạn sử dụng cấu trúc ACL được trả về từ phương thức `get_acl_access_data` và kiểm tra nó so với các tập tin được liệt kê trong mỗi commit, bạn có thể xác định xem người dùng có quyền truy cập để đẩy tất cả các commit của họ hay không: [source,ruby]
# only allows certain users to modify certain subdirectories in a project def check_directory_perms access = get_acl_access_data('acl')
# see if anyone is trying to push something they can't
new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
new_commits.each do |rev|
files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n")
files_modified.each do |path|
next if path.size == 0
has_file_access = false
access[$user].each do |access_path|
if !access_path # user has access to everything
|| (path.start_with? access_path) # access to this path
has_file_access = true
end
end
if !has_file_access
puts "[POLICY] You do not have access to push to #{path}"
exit 1
end
end
end
end
check_directory_perms
Bạn nhận được danh sách các commit mới đang được đẩy lên máy chủ của mình với `git rev-list`. Sau đó, đối với mỗi commit đó, bạn tìm tập tin nào được sửa đổi và đảm bảo người dùng đang đẩy có quyền truy cập vào tất cả các đường dẫn đang được sửa đổi. Bây giờ người dùng của bạn không thể đẩy bất kỳ commit nào với thông điệp được định dạng sai hoặc với các tập tin được sửa đổi bên ngoài đường dẫn được chỉ định của họ. ===== Kiểm tra Thử Nếu bạn chạy `chmod u+x .git/hooks/update`, đây là tập tin mà bạn lẽ ra phải đặt tất cả mã này vào, và sau đó cố gắng đẩy một commit với thông điệp không tuân thủ, bạn sẽ nhận được một cái gì đó như thế này: [source,console]
$ git push -f origin master Counting objects: 5, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 323 bytes, done. Total 3 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. Enforcing Policies… (refs/heads/master) (8338c5) (c5b616) [POLICY] Your message is not formatted correctly error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master To git@gitserver:project.git ! [remote rejected] master → master (hook declined) error: failed to push some refs to 'git@gitserver:project.git'
Có một vài điều thú vị ở đây. Đầu tiên, bạn thấy điều này nơi móc bắt đầu chạy. [source,console]
Enforcing Policies… (refs/heads/master) (fb8c72) (c56860)
Hãy nhớ rằng bạn đã in ra điều đó ngay từ đầu tập tin kịch bản cập nhật của mình. Bất cứ điều gì tập tin kịch bản của bạn echo ra `stdout` sẽ được chuyển đến máy khách. Điều tiếp theo bạn sẽ nhận thấy là thông báo lỗi. [source,console]
[POLICY] Your message is not formatted correctly error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master
Dòng đầu tiên được in ra bởi bạn, hai dòng còn lại là Git nói với bạn rằng tập tin kịch bản cập nhật đã thoát với mã khác không và đó là lý do từ chối lần đẩy của bạn. Cuối cùng, bạn có cái này: [source,console]
To git@gitserver:project.git ! [remote rejected] master → master (hook declined) error: failed to push some refs to 'git@gitserver:project.git'
Bạn sẽ thấy thông báo từ chối từ xa cho mỗi tham chiếu mà móc của bạn đã từ chối, và nó cho bạn biết rằng nó đã bị từ chối cụ thể vì lỗi móc. Hơn nữa, nếu ai đó cố gắng chỉnh sửa một tập tin mà họ không có quyền truy cập và đẩy một commit chứa nó, họ sẽ thấy một cái gì đó tương tự. Ví dụ, nếu một tác giả tài liệu cố gắng đẩy một commit sửa đổi thứ gì đó trong thư mục `lib`, họ sẽ thấy: [source,console]
[POLICY] You do not have access to push to lib/test.rb
Từ giờ trở đi, miễn là tập tin kịch bản `update` đó ở đó và có thể thực thi được, kho chứa của bạn sẽ không bao giờ có một thông điệp commit nào mà không có mẫu của bạn trong đó, và người dùng của bạn sẽ bị đóng hộp (sandboxed). ==== Móc Phía Máy Khách Nhược điểm của phương pháp này là sự than vãn chắc chắn sẽ xảy ra khi các lần đẩy commit của người dùng của bạn bị từ chối. Việc công việc được chế tác cẩn thận của họ bị từ chối vào phút cuối có thể cực kỳ bực bội và khó hiểu; và hơn nữa, họ sẽ phải chỉnh sửa lịch sử của mình để sửa nó, điều này không phải lúc nào cũng dành cho những người yếu tim. Câu trả lời cho tình thế tiến thoái lưỡng nan này là cung cấp một số móc phía máy khách mà người dùng có thể chạy để thông báo cho họ khi họ đang làm điều gì đó mà máy chủ có khả năng từ chối. Bằng cách đó, họ có thể sửa bất kỳ vấn đề nào trước khi commit và trước khi những vấn đề đó trở nên khó sửa hơn. Bởi vì các móc không được chuyển cùng với bản sao (clone) của một dự án, bạn phải phân phối các tập tin kịch bản này theo một cách khác và sau đó yêu cầu người dùng của bạn sao chép chúng vào thư mục `.git/hooks` của họ và làm cho chúng có thể thực thi được. Bạn có thể phân phối các móc này trong dự án hoặc trong một dự án riêng biệt, nhưng Git sẽ không tự động thiết lập chúng. Để bắt đầu, bạn nên kiểm tra thông điệp commit của mình ngay trước khi mỗi commit được ghi lại, để bạn biết máy chủ sẽ không từ chối các thay đổi của bạn do các thông điệp commit được định dạng sai. Để làm điều này, bạn có thể thêm móc `commit-msg`. Nếu bạn để nó đọc thông điệp từ tập tin được truyền làm đối số đầu tiên và so sánh nó với mẫu, bạn có thể buộc Git hủy bỏ commit nếu không có sự trùng khớp: [source,ruby]
#!/usr/bin/env ruby message_file = ARGV[0] message = File.read(message_file)
$regex = /\[ref: (\d+)\]/
if !$regex.match(message) puts "[POLICY] Your message is not formatted correctly" exit 1 end
Nếu tập tin kịch bản đó ở đúng vị trí (trong `.git/hooks/commit-msg`) và có thể thực thi được, và bạn commit với một thông điệp không được định dạng đúng, bạn sẽ thấy điều này: [source,console]
$ git commit -am 'Test' [POLICY] Your message is not formatted correctly
Không có commit nào được hoàn thành trong trường hợp đó. Tuy nhiên, nếu thông điệp của bạn chứa mẫu thích hợp, Git cho phép bạn commit: [source,console]
$ git commit -am 'Test [ref: 132]'
1 file changed, 1 insertions(+), 0 deletions(-)
Tiếp theo, bạn muốn đảm bảo rằng bạn không sửa đổi các tập tin nằm ngoài phạm vi ACL của mình. Nếu thư mục `.git` của dự án của bạn chứa một bản sao của tập tin ACL bạn đã sử dụng trước đó, thì tập tin kịch bản `pre-commit` sau đây sẽ thực thi các ràng buộc đó cho bạn: [source,ruby]
#!/usr/bin/env ruby
$user = ENV['USER']
# [ insert acl_access_data method from above ]
# only allows certain users to modify certain subdirectories in a project def check_directory_perms access = get_acl_access_data('.git/acl')
files_modified = `git diff-index --cached --name-only HEAD`.split("\n")
files_modified.each do |path|
next if path.size == 0
has_file_access = false
access[$user].each do |access_path|
if !access_path || (path.index(access_path) == 0)
has_file_access = true
end
if !has_file_access
puts "[POLICY] You do not have access to push to #{path}"
exit 1
end
end
end
check_directory_perms
Đây gần như là cùng một tập tin kịch bản với phần phía máy chủ, nhưng với hai sự khác biệt quan trọng. Đầu tiên, tập tin ACL ở một nơi khác, bởi vì tập tin kịch bản này chạy từ thư mục làm việc của bạn, không phải từ thư mục `.git` của bạn. Bạn phải thay đổi đường dẫn đến tập tin ACL từ thế này: [source,ruby]
access = get_acl_access_data('acl')
thành thế này: [source,ruby]
access = get_acl_access_data('.git/acl')
Sự khác biệt quan trọng khác là cách bạn lấy danh sách các tập tin đã bị thay đổi. Bởi vì phương pháp phía máy chủ xem xét nhật ký của các commit, và, tại thời điểm này, commit chưa được ghi lại, bạn phải lấy danh sách tập tin của mình từ khu vực tổ chức (staging area) thay thế. Thay vì: [source,ruby]
files_modified = git log -1 --name-only --pretty=format:'' #{ref}
bạn phải sử dụng: [source,ruby]
files_modified = git diff-index --cached --name-only HEAD
Nhưng đó là hai sự khác biệt duy nhất – nếu không, tập tin kịch bản hoạt động theo cùng một cách. Một lưu ý là nó mong đợi bạn đang chạy cục bộ với tư cách là cùng một người dùng mà bạn đẩy lên máy từ xa. Nếu khác nhau, bạn phải thiết lập biến `$user` một cách thủ công. Một điều khác chúng ta có thể làm ở đây là đảm bảo người dùng không đẩy các tham chiếu không được tua nhanh (non-fast-forwarded). Để có được một tham chiếu không phải là tua nhanh, bạn hoặc phải rebase qua một commit mà bạn đã đẩy lên hoặc cố gắng đẩy một nhánh cục bộ khác lên cùng một nhánh từ xa. Có lẽ, máy chủ đã được cấu hình với `receive.denyDeletes` và `receive.denyNonFastForwards` để thực thi chính sách này, vì vậy điều tình cờ duy nhất bạn có thể cố gắng bắt là rebase các commit đã được đẩy. Đây là một ví dụ về tập tin kịch bản pre-rebase kiểm tra điều đó. Nó lấy danh sách tất cả các commit bạn sắp viết lại và kiểm tra xem chúng có tồn tại trong bất kỳ tham chiếu từ xa nào của bạn hay không. Nếu nó thấy một cái có thể truy cập được từ một trong các tham chiếu từ xa của bạn, nó sẽ hủy bỏ việc rebase. [source,ruby]
#!/usr/bin/env ruby
base_branch = ARGV[0] if ARGV[1] topic_branch = ARGV[1] else topic_branch = "HEAD" end
target_shas = git rev-list {base_branch}..{topic_branch}.split("\n")
remote_refs = git branch -r.split("\n").map { |r| r.strip }
target_shas.each do |sha|
remote_refs.each do |remote_ref|
shas_pushed = git rev-list {sha}@ refs/remotes/{remote_ref}
if shas_pushed.split("\n").include?(sha)
puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}"
exit 1
end
end
end
Tập tin kịch bản này sử dụng một cú pháp chưa được đề cập trong <<ch07-git-tools#_revision_selection>>. Bạn nhận được danh sách các commit đã được đẩy lên bằng cách chạy lệnh này: [source,ruby]
git rev-list {sha}@ refs/remotes/{remote_ref}
Cú pháp `SHA^@` phân giải thành tất cả các cha của commit đó. Bạn đang tìm kiếm bất kỳ commit nào có thể truy cập được từ commit cuối cùng trên máy từ xa và không thể truy cập được từ bất kỳ cha nào của bất kỳ SHA-1 nào bạn đang cố gắng đẩy lên – nghĩa là nó là một tua nhanh (fast-forward). Hạn chế chính của phương pháp này là nó có thể rất chậm và thường không cần thiết – nếu bạn không cố gắng buộc việc đẩy bằng `-f`, máy chủ sẽ cảnh báo bạn và không chấp nhận lần đẩy. Tuy nhiên, đó là một bài tập thú vị và về lý thuyết có thể giúp bạn tránh việc rebase mà sau này bạn có thể phải quay lại và sửa. === Tóm tắt Chúng ta đã trình bày hầu hết các cách chính để tuỳ biến client và server Git sao cho phù hợp nhất với luồng công việc và dự án của bạn. Bạn đã học về nhiều thiết lập cấu hình, thuộc tính tệp và hook sự kiện, và đã xây dựng ví dụ về máy chủ thực thi chính sách. Bây giờ bạn có thể làm cho Git phù hợp với hầu như bất kỳ luồng công việc nào bạn nghĩ tới. [[ch09-git-and-other-systems]] == Git và các hệ thống khác Thế giới không hoàn hảo. Thông thường, bạn không thể ngay lập tức chuyển mọi dự án mà bạn gặp sang Git. Đôi khi bạn bị kẹt với một dự án dùng VCS khác và ước gì nó dùng Git. Phần đầu chương này sẽ tìm hiểu cách dùng Git như một client khi dự án bạn làm việc được lưu trữ trên hệ thống khác. Đến một lúc nào đó, bạn có thể muốn chuyển dự án hiện có sang Git. Phần thứ hai của chương trình bày cách di chuyển dự án sang Git từ một số hệ thống cụ thể, cũng như một phương pháp chung khi không có công cụ import sẵn. === Git như một Client (((Git as a client))) Git mang lại trải nghiệm tốt cho nhà phát triển đến mức nhiều người đã tìm cách dùng nó trên máy trạm của mình, ngay cả khi phần còn lại của nhóm dùng một VCS hoàn toàn khác. Có một số bộ chuyển đổi như vậy, gọi là "bridges", sẵn có. Ở đây chúng tôi sẽ đề cập tới những bộ chuyển đổi mà bạn có khả năng gặp nhất. [[_git_svn]] ==== Git và Subversion (((Subversion)))(((Interoperation with other VCSs, Subversion))) Một phần lớn các dự án phát triển mã nguồn mở và một số lượng lớn các dự án doanh nghiệp sử dụng Subversion để quản lý mã nguồn của họ. Nó đã tồn tại hơn một thập kỷ, và trong phần lớn thời gian đó là lựa chọn VCS _thực tế_ (de facto) cho các dự án mã nguồn mở. Nó cũng rất giống về nhiều mặt với CVS, vốn là ông lớn của thế giới kiểm soát nguồn trước đó. (((git commands, svn)))(((git-svn))) Một trong những tính năng tuyệt vời của Git là một cầu nối hai chiều đến Subversion được gọi là `git svn`. Công cụ này cho phép bạn sử dụng Git như một máy khách hợp lệ cho máy chủ Subversion, vì vậy bạn có thể sử dụng tất cả các tính năng cục bộ của Git và sau đó đẩy lên máy chủ Subversion như thể bạn đang sử dụng Subversion cục bộ. Điều này có nghĩa là bạn có thể thực hiện phân nhánh và trộn cục bộ, sử dụng khu vực tổ chức (staging area), sử dụng rebase và cherry-pick, và vân vân, trong khi các cộng tác viên của bạn tiếp tục làm việc theo những cách đen tối và cổ xưa của họ. Đó là một cách tốt để đưa Git vào môi trường doanh nghiệp và giúp các nhà phát triển đồng nghiệp của bạn trở nên hiệu quả hơn trong khi bạn vận động để thay đổi cơ sở hạ tầng để hỗ trợ Git hoàn toàn. Cầu nối Subversion là liều thuốc dẫn vào thế giới DVCS. ===== `git svn` Lệnh cơ sở trong Git cho tất cả các lệnh cầu nối Subversion là `git svn`. Nó nhận khá nhiều lệnh, vì vậy chúng tôi sẽ hiển thị những lệnh phổ biến nhất trong khi đi qua một vài quy trình làm việc đơn giản. Điều quan trọng cần lưu ý là khi bạn đang sử dụng `git svn`, bạn đang tương tác với Subversion, một hệ thống hoạt động rất khác so với Git. Mặc dù bạn *có thể* thực hiện phân nhánh và trộn cục bộ, nhưng nói chung tốt nhất là giữ cho lịch sử của bạn tuyến tính nhất có thể bằng cách rebase công việc của bạn, và tránh làm những việc như tương tác đồng thời với một kho chứa Git từ xa. Đừng viết lại lịch sử của bạn và cố gắng đẩy lại, và đừng đẩy đến một kho chứa Git song song để cộng tác với các nhà phát triển Git đồng nghiệp cùng một lúc. Subversion chỉ có thể có một lịch sử tuyến tính duy nhất, và làm nó bối rối là rất dễ dàng. Nếu bạn đang làm việc với một nhóm, và một số đang sử dụng SVN và những người khác đang sử dụng Git, hãy đảm bảo mọi người đều đang sử dụng máy chủ SVN để cộng tác – làm như vậy sẽ giúp cuộc sống của bạn dễ dàng hơn. ===== Thiết lập Để minh họa chức năng này, bạn cần một kho chứa SVN điển hình mà bạn có quyền ghi. Nếu bạn muốn sao chép các ví dụ này, bạn sẽ phải tạo một bản sao có thể ghi của một kho chứa thử nghiệm SVN. Để làm điều đó một cách dễ dàng, bạn có thể sử dụng một công cụ gọi là `svnsync` đi kèm với Subversion. Để làm theo, trước tiên bạn cần tạo một kho chứa Subversion cục bộ mới: [source,console]
$ mkdir /tmp/test-svn $ svnadmin create /tmp/test-svn
Sau đó, cho phép tất cả người dùng thay đổi revprops – cách dễ nhất là thêm một tập tin kịch bản `pre-revprop-change` luôn thoát 0: [source,console]
$ cat /tmp/test-svn/hooks/pre-revprop-change #!/bin/sh exit 0; $ chmod +x /tmp/test-svn/hooks/pre-revprop-change
Bây giờ bạn có thể đồng bộ dự án này với máy cục bộ của mình bằng cách gọi `svnsync init` với các kho chứa đến và đi. [source,console]
$ svnsync init file:///tmp/test-svn \ http://your-svn-server.example.org/svn/
Điều này thiết lập các thuộc tính để chạy đồng bộ hóa. Sau đó, bạn có thể sao chép mã bằng cách chạy: [source,console]
$ svnsync sync file:///tmp/test-svn Committed revision 1. Copied properties for revision 1. Transmitting file data ………………………..[…] Committed revision 2. Copied properties for revision 2. […]
Mặc dù thao tác này có thể chỉ mất vài phút, nhưng nếu bạn cố gắng sao chép kho chứa gốc sang một kho chứa từ xa khác thay vì một kho chứa cục bộ, quá trình này sẽ mất gần một giờ, ngay cả khi có ít hơn 100 commit. Subversion phải sao chép từng bản sửa đổi một và sau đó đẩy nó trở lại vào một kho chứa khác – nó cực kỳ kém hiệu quả, nhưng đó là cách dễ dàng duy nhất để làm điều này. ===== Bắt đầu Bây giờ bạn đã có một kho chứa Subversion mà bạn có quyền ghi, bạn có thể đi qua một quy trình làm việc điển hình. Bạn sẽ bắt đầu với lệnh `git svn clone`, lệnh này nhập toàn bộ kho chứa Subversion vào một kho chứa Git cục bộ. Hãy nhớ rằng nếu bạn đang nhập từ một kho chứa Subversion được lưu trữ thực sự, bạn nên thay thế `\file:///tmp/test-svn` ở đây bằng URL của kho chứa Subversion của bạn: [source,console]
$ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags Initialized empty Git repository in /private/tmp/progit/test-svn/.git/ r1 = dcbfb5891860124cc2e8cc616cded42624897125 (refs/remotes/origin/trunk) A m4/acx_pthread.m4 A m4/stl_hash.m4 A java/src/test/java/com/google/protobuf/UnknownFieldSetTest.java A java/src/test/java/com/google/protobuf/WireFormatTest.java … r75 = 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae (refs/remotes/origin/trunk) Found possible branch point: file:///tmp/test-svn/trunk ⇒ file:///tmp/test-svn/branches/my-calc-branch, 75 Found branch parent: (refs/remotes/origin/my-calc-branch) 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae Following parent with do_switch Successfully followed parent r76 = 0fb585761df569eaecd8146c71e58d70147460a2 (refs/remotes/origin/my-calc-branch) Checked out HEAD: file:///tmp/test-svn/trunk r75
Điều này chạy tương đương với hai lệnh – `git svn init` theo sau là `git svn fetch` – trên URL bạn cung cấp. Điều này có thể mất một lúc. Ví dụ, nếu dự án thử nghiệm chỉ có khoảng 75 commit và cơ sở mã không quá lớn, Git vẫn phải check out từng phiên bản, từng cái một, và commit nó riêng lẻ. Đối với một dự án có hàng trăm hoặc hàng nghìn commit, điều này thực sự có thể mất hàng giờ hoặc thậm chí hàng ngày để hoàn thành. Phần `-T trunk -b branches -t tags` bảo Git rằng kho chứa Subversion này tuân theo các quy ước phân nhánh và gắn thẻ cơ bản. Nếu bạn đặt tên trunk, branches, hoặc tags khác đi, bạn có thể thay đổi các tùy chọn này. Bởi vì điều này rất phổ biến, bạn có thể thay thế toàn bộ phần này bằng `-s`, có nghĩa là bố cục tiêu chuẩn (standard layout) và ngụ ý tất cả các tùy chọn đó. Lệnh sau đây là tương đương: [source,console]
$ git svn clone file:///tmp/test-svn -s
Tại thời điểm này, bạn nên có một kho chứa Git hợp lệ đã nhập các nhánh và thẻ của bạn: [source,console]
$ git branch -a * master remotes/origin/my-calc-branch remotes/origin/tags/2.0.2 remotes/origin/tags/release-2.0.1 remotes/origin/tags/release-2.0.2 remotes/origin/tags/release-2.0.2rc1 remotes/origin/trunk
Lưu ý cách công cụ này quản lý các thẻ Subversion như các tham chiếu từ xa. (((git commands, show-ref))) Hãy xem xét kỹ hơn với lệnh plumbing Git `show-ref`: [source,console]
$ git show-ref 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/heads/master 0fb585761df569eaecd8146c71e58d70147460a2 refs/remotes/origin/my-calc-branch bfd2d79303166789fc73af4046651a4b35c12f0b refs/remotes/origin/tags/2.0.2 285c2b2e36e467dd4d91c8e3c0c0e1750b3fe8ca refs/remotes/origin/tags/release-2.0.1 cbda99cb45d9abcb9793db1d4f70ae562a969f1e refs/remotes/origin/tags/release-2.0.2 a9f074aa89e826d6f9d30808ce5ae3ffe711feda refs/remotes/origin/tags/release-2.0.2rc1 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/remotes/origin/trunk
Git không làm điều này khi nó sao chép từ một máy chủ Git; đây là những gì một kho chứa với các thẻ trông như thế nào sau khi sao chép mới: [source,console]
$ git show-ref c3dcbe8488c6240392e8a5d7553bbffcb0f94ef0 refs/remotes/origin/master 32ef1d1c7cc8c603ab78416262cc421b80a8c2df refs/remotes/origin/branch-1 75f703a3580a9b81ead89fe1138e6da858c5ba18 refs/remotes/origin/branch-2 23f8588dde934e8f33c263c6d8359b2ae095f863 refs/tags/v0.1.0 7064938bd5e7ef47bfd79a685a62c1e2649e2ce7 refs/tags/v0.2.0 6dcb09b5b57875f334f61aebed695e2e4193db5e refs/tags/v1.0.0
Git lấy các thẻ trực tiếp vào `refs/tags`, thay vì coi chúng như các nhánh từ xa. ===== Commit Trở lại Subversion Bây giờ bạn đã có một thư mục làm việc, bạn có thể thực hiện một số công việc trên dự án và đẩy các commit của bạn trở lại thượng nguồn (upstream), sử dụng Git một cách hiệu quả như một máy khách SVN. Nếu bạn chỉnh sửa một trong các tập tin và commit nó, bạn có một commit tồn tại trong Git cục bộ mà không tồn tại trên máy chủ Subversion: [source,console]
$ git commit -am 'Adding git-svn instructions to the README' [master 4af61fd] Adding git-svn instructions to the README 1 file changed, 5 insertions(+)
Tiếp theo, bạn cần đẩy thay đổi của mình lên thượng nguồn. Lưu ý cách điều này thay đổi cách bạn làm việc với Subversion – bạn có thể thực hiện một vài commit ngoại tuyến và sau đó đẩy tất cả chúng cùng một lúc lên máy chủ Subversion. Để đẩy lên một máy chủ Subversion, bạn chạy lệnh `git svn dcommit`: [source,console]
$ git svn dcommit Committing to file:///tmp/test-svn/trunk … M README.txt Committed r77 M README.txt r77 = 95e0222ba6399739834380eb10afcd73e0670bc5 (refs/remotes/origin/trunk) No changes between 4af61fd05045e07598c553167e0f31c84fd6ffe1 and refs/remotes/origin/trunk Resetting to the latest refs/remotes/origin/trunk
Lệnh này lấy tất cả các commit bạn đã thực hiện trên mã máy chủ Subversion, thực hiện một commit Subversion cho mỗi cái, và sau đó viết lại commit Git cục bộ của bạn để bao gồm một định danh duy nhất. Điều này quan trọng vì nó có nghĩa là tất cả các tổng kiểm tra SHA-1 cho các commit của bạn đều thay đổi. Một phần vì lý do này, làm việc với các phiên bản từ xa dựa trên Git của các dự án của bạn đồng thời với một máy chủ Subversion không phải là một ý tưởng hay. Nếu bạn nhìn vào commit cuối cùng, bạn có thể thấy `git-svn-id` mới đã được thêm vào: [source,console]
$ git log -1 commit 95e0222ba6399739834380eb10afcd73e0670bc5 Author: ben <ben@0b684db3-b064-4277-89d1-21af03df0a68> Date: Thu Jul 24 03:08:36 2014 +0000
Adding git-svn instructions to the README
git-svn-id: file:///tmp/test-svn/trunk@77 0b684db3-b064-4277-89d1-21af03df0a68
Lưu ý rằng tổng kiểm tra SHA-1 ban đầu bắt đầu bằng `4af61fd` khi bạn commit bây giờ bắt đầu bằng `95e0222`. Nếu bạn muốn đẩy lên cả máy chủ Git và máy chủ Subversion, bạn phải đẩy (`dcommit`) lên máy chủ Subversion trước, bởi vì hành động đó thay đổi dữ liệu commit của bạn. ===== Kéo vào Các Thay đổi Mới Nếu bạn đang làm việc với các nhà phát triển khác, thì tại một thời điểm nào đó, một trong số các bạn sẽ đẩy, và sau đó người kia sẽ cố gắng đẩy một thay đổi gây xung đột. Thay đổi đó sẽ bị từ chối cho đến khi bạn trộn công việc của họ vào. Trong `git svn`, nó trông như thế này: [source,console]
$ git svn dcommit Committing to file:///tmp/test-svn/trunk …
ERROR from SVN: Transaction is out of date: File '/trunk/README.txt' is out of date W: d5837c4b461b7c0e018b49d12398769d2bfc240a and refs/remotes/origin/trunk differ, using rebase: :100644 100644 f414c433af0fd6734428cf9d2a9fd8ba00ada145 c80b6127dd04f5fcda218730ddf3a2da4eb39138 M README.txt Current branch master is up to date. ERROR: Not all changes have been committed into SVN, however the committed ones (if any) seem to be successfully integrated into the working tree. Please see the above messages for details.
Để giải quyết tình huống này, bạn có thể chạy `git svn rebase`, lệnh này kéo xuống bất kỳ thay đổi nào trên máy chủ mà bạn chưa có và rebase bất kỳ công việc nào bạn có lên trên những gì có trên máy chủ: [source,console]
$ git svn rebase Committing to file:///tmp/test-svn/trunk …
ERROR from SVN: Transaction is out of date: File '/trunk/README.txt' is out of date W: eaa029d99f87c5c822c5c29039d19111ff32ef46 and refs/remotes/origin/trunk differ, using rebase: :100644 100644 65536c6e30d263495c17d781962cfff12422693a b34372b25ccf4945fe5658fa381b075045e7702a M README.txt First, rewinding head to replay your work on top of it… Applying: update foo Using index info to reconstruct a base tree… M README.txt Falling back to patching base and 3-way merge… Auto-merging README.txt ERROR: Not all changes have been committed into SVN, however the committed ones (if any) seem to be successfully integrated into the working tree. Please see the above messages for details.
Bây giờ, tất cả công việc của bạn đều nằm trên những gì có trên máy chủ Subversion, vì vậy bạn có thể `dcommit` thành công: [source,console]
$ git svn dcommit Committing to file:///tmp/test-svn/trunk … M README.txt Committed r85 M README.txt r85 = 9c29704cc0bbbed7bd58160cfb66cb9191835cd8 (refs/remotes/origin/trunk) No changes between 5762f56732a958d6cfda681b661d2a239cc53ef5 and refs/remotes/origin/trunk Resetting to the latest refs/remotes/origin/trunk
Lưu ý rằng không giống như Git, yêu cầu bạn trộn công việc thượng nguồn mà bạn chưa có cục bộ trước khi bạn có thể đẩy, `git svn` bắt bạn làm điều đó chỉ khi các thay đổi xung đột (giống như cách Subversion hoạt động). Nếu người khác đẩy một thay đổi vào một tập tin và sau đó bạn đẩy một thay đổi vào một tập tin khác, `dcommit` của bạn sẽ hoạt động tốt: [source,console]
$ git svn dcommit Committing to file:///tmp/test-svn/trunk … M configure.ac Committed r87 M autogen.sh r86 = d8450bab8a77228a644b7dc0e95977ffc61adff7 (refs/remotes/origin/trunk) M configure.ac r87 = f3653ea40cb4e26b6281cec102e35dcba1fe17c4 (refs/remotes/origin/trunk) W: a0253d06732169107aa020390d9fefd2b1d92806 and refs/remotes/origin/trunk differ, using rebase: :100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 e757b59a9439312d80d5d43bb65d4a7d0389ed6d M autogen.sh First, rewinding head to replay your work on top of it…
Điều quan trọng cần nhớ là kết quả là một trạng thái dự án không tồn tại trên cả hai máy tính của bạn khi bạn đẩy. Nếu các thay đổi không tương thích nhưng không xung đột, bạn có thể gặp các vấn đề khó chẩn đoán. Điều này khác với việc sử dụng máy chủ Git – trong Git, bạn có thể kiểm tra đầy đủ trạng thái trên hệ thống máy khách của mình trước khi xuất bản nó, trong khi trong SVN, bạn không bao giờ có thể chắc chắn rằng các trạng thái ngay trước khi commit và sau khi commit là giống hệt nhau. Bạn cũng nên chạy lệnh này để kéo vào các thay đổi từ máy chủ Subversion, ngay cả khi bạn chưa sẵn sàng commit. Bạn có thể chạy `git svn fetch` để lấy dữ liệu mới, nhưng `git svn rebase` thực hiện việc lấy và sau đó cập nhật các commit cục bộ của bạn. [source,console]
$ git svn rebase M autogen.sh r88 = c9c5f83c64bd755368784b444bc7a0216cc1e17b (refs/remotes/origin/trunk) First, rewinding head to replay your work on top of it… Fast-forwarded master to refs/remotes/origin/trunk.
Chạy `git svn rebase` thỉnh thoảng đảm bảo mã của bạn luôn được cập nhật. Tuy nhiên, bạn cần chắc chắn rằng thư mục làm việc của mình sạch sẽ khi bạn chạy lệnh này. Nếu bạn có các thay đổi cục bộ, bạn phải stash công việc của mình hoặc commit tạm thời trước khi chạy `git svn rebase` – nếu không, lệnh sẽ dừng lại nếu nó thấy rằng việc rebase sẽ dẫn đến xung đột trộn. ===== Các Vấn đề Phân nhánh Git Khi bạn đã trở nên thoải mái với quy trình làm việc Git, bạn có thể sẽ tạo các nhánh chủ đề, thực hiện công việc trên chúng, và sau đó trộn chúng vào. Nếu bạn đang đẩy lên máy chủ Subversion thông qua `git svn`, bạn có thể muốn rebase công việc của mình lên một nhánh duy nhất mỗi lần thay vì trộn các nhánh lại với nhau. Lý do để ưu tiên rebase là Subversion có lịch sử tuyến tính và không xử lý các lần trộn như Git, vì vậy `git svn` chỉ theo dõi cha mẹ đầu tiên khi chuyển đổi các ảnh chụp nhanh thành các commit Subversion. Giả sử lịch sử của bạn trông giống như sau: bạn đã tạo một nhánh `experiment`, thực hiện hai commit, và sau đó trộn chúng trở lại vào `master`. Khi bạn `dcommit`, bạn thấy đầu ra như thế này: [source,console]
$ git svn dcommit Committing to file:///tmp/test-svn/trunk … M CHANGES.txt Committed r89 M CHANGES.txt r89 = 89d492c884ea7c834353563d5d913c6adf933981 (refs/remotes/origin/trunk) M COPYING.txt M INSTALL.txt Committed r90 M INSTALL.txt M COPYING.txt r90 = cb522197870e61467473391799148f6721bcf9a0 (refs/remotes/origin/trunk) No changes between 71af502c214ba13123992338569f4669877f55fd and refs/remotes/origin/trunk Resetting to the latest refs/remotes/origin/trunk
Chạy `dcommit` trên một nhánh với lịch sử đã trộn hoạt động tốt, ngoại trừ việc khi bạn nhìn vào lịch sử dự án Git của mình, nó chưa viết lại bất kỳ commit nào bạn đã thực hiện trên nhánh `experiment` – thay vào đó, tất cả các thay đổi đó xuất hiện trong phiên bản SVN của commit trộn duy nhất. Khi người khác sao chép công việc đó, tất cả những gì họ thấy là commit trộn với tất cả công việc được squash vào đó, như thể bạn đã chạy `git merge --squash`; họ không thấy dữ liệu commit về việc nó đến từ đâu hoặc khi nào nó được commit. ===== Phân nhánh Subversion Phân nhánh trong Subversion không giống như phân nhánh trong Git; nếu bạn có thể tránh sử dụng nó nhiều, đó có lẽ là tốt nhất. Tuy nhiên, bạn có thể tạo và commit vào các nhánh trong Subversion bằng cách sử dụng `git svn`. ===== Tạo một Nhánh SVN Mới Để tạo một nhánh mới trong Subversion, bạn chạy `git svn branch [new-branch]`: [source,console]
$ git svn branch opera Copying file:///tmp/test-svn/trunk at r90 to file:///tmp/test-svn/branches/opera…; Found possible branch point: file:///tmp/test-svn/trunk ⇒ file:///tmp/test-svn/branches/opera, 90 Found branch parent: (refs/remotes/origin/opera) cb522197870e61467473391799148f6721bcf9a0 Following parent with do_switch Successfully followed parent r91 = f1b64a3855d3c8dd84ee0ef10fa89d27f1584302 (refs/remotes/origin/opera)
Điều này thực hiện tương đương với lệnh `svn copy trunk branches/opera` trong Subversion và hoạt động trên máy chủ Subversion. Điều quan trọng cần lưu ý là nó không check out bạn vào nhánh đó; nếu bạn commit tại thời điểm này, commit đó sẽ đi đến `trunk` trên máy chủ, không phải `opera`. ===== Chuyển đổi Các Nhánh Hoạt động Git tìm ra nhánh nào dcommits của bạn đi đến bằng cách tìm kiếm đỉnh của bất kỳ nhánh Subversion nào của bạn trong lịch sử của bạn – bạn chỉ nên có một, và nó nên là cái cuối cùng có `git-svn-id` trong lịch sử nhánh hiện tại của bạn. Nếu bạn muốn làm việc trên nhiều hơn một nhánh đồng thời, bạn có thể thiết lập các nhánh cục bộ để `dcommit` đến các nhánh Subversion cụ thể bằng cách bắt đầu chúng tại commit Subversion đã nhập cho nhánh đó. Nếu bạn muốn một nhánh `opera` mà bạn có thể làm việc riêng biệt, bạn có thể chạy: [source,console]
$ git branch opera remotes/origin/opera
Bây giờ, nếu bạn muốn trộn nhánh `opera` của mình vào `trunk` (nhánh `master` của bạn), bạn có thể làm như vậy với một `git merge` bình thường. Nhưng bạn cần cung cấp một thông điệp commit mô tả (thông qua `-m`), nếu không việc trộn sẽ nói "`Merge branch opera`" thay vì một cái gì đó hữu ích. Hãy nhớ rằng mặc dù bạn đang sử dụng `git merge` để thực hiện thao tác này, và việc trộn có thể sẽ dễ dàng hơn nhiều so với trong Subversion (vì Git sẽ tự động phát hiện cơ sở trộn thích hợp cho bạn), đây không phải là một commit trộn Git bình thường. Bạn phải đẩy dữ liệu này trở lại một máy chủ Subversion không thể xử lý một commit theo dõi nhiều hơn một cha mẹ; vì vậy, sau khi bạn đẩy nó lên, nó sẽ trông giống như một commit duy nhất đã squash tất cả công việc của một nhánh khác dưới một commit duy nhất. Sau khi bạn trộn một nhánh vào một nhánh khác, bạn không thể dễ dàng quay lại và tiếp tục làm việc trên nhánh đó, như bạn thường có thể làm trong Git. Lệnh `dcommit` mà bạn chạy xóa bất kỳ thông tin nào nói rằng nhánh nào đã được trộn vào, vì vậy các tính toán cơ sở trộn tiếp theo sẽ sai – `dcommit` làm cho kết quả `git merge` của bạn trông giống như bạn đã chạy `git merge --squash`. Thật không may, không có cách nào tốt để tránh tình huống này – Subversion không thể lưu trữ thông tin này, vì vậy bạn sẽ luôn bị tê liệt bởi những hạn chế của nó trong khi bạn đang sử dụng nó làm máy chủ của mình. Để tránh các vấn đề, bạn nên xóa nhánh cục bộ (trong trường hợp này, `opera`) sau khi bạn trộn nó vào trunk. ===== Các Lệnh Subversion Bộ công cụ `git svn` cung cấp một số lệnh để giúp giảm bớt quá trình chuyển đổi sang Git bằng cách cung cấp một số chức năng tương tự như những gì bạn có trong Subversion. Dưới đây là một vài lệnh cung cấp cho bạn những gì Subversion đã từng làm. ====== Lịch sử Kiểu SVN Nếu bạn đã quen với Subversion và muốn xem lịch sử của mình theo kiểu đầu ra SVN, bạn có thể chạy `git svn log` để xem lịch sử commit của mình theo định dạng SVN: [source,console]
$ git svn log
r87 | schacon | 2014-05-02 16:07:37 -0700 (Sat, 02 May 2014) | 2 lines autogen change
r86 | schacon | 2014-05-02 16:00:21 -0700 (Sat, 02 May 2014) | 2 lines
Merge branch 'experiment'
r85 | schacon | 2014-05-02 16:00:09 -0700 (Sat, 02 May 2014) | 2 lines
updated the changelog
----
Bạn nên biết hai điều quan trọng về `git svn log`.
Đầu tiên, nó hoạt động ngoại tuyến, không giống như lệnh `svn log` thực sự, lệnh này yêu cầu máy chủ Subversion cung cấp dữ liệu.
Thứ hai, nó chỉ hiển thị cho bạn các commit đã được commit lên máy chủ Subversion.
Các commit Git cục bộ mà bạn chưa dcommited không hiển thị; các commit mà mọi người đã thực hiện cho máy chủ Subversion trong thời gian đó cũng không hiển thị.
Nó giống như trạng thái đã biết cuối cùng của các commit trên máy chủ Subversion.
====== Chú thích SVN
Cũng giống như lệnh `git svn log` mô phỏng lệnh `svn log` ngoại tuyến, bạn có thể nhận được tương đương của `svn annotate` bằng cách chạy `git svn blame [FILE]`.
Đầu ra trông giống như thế này:
[source,console]
----
$ git svn blame README.txt
2 temporal Protocol Buffers - Google's data interchange format
2 temporal Copyright 2008 Google Inc.
2 temporal http://code.google.com/apis/protocolbuffers/
2 temporal
22 temporal C++ Installation - Unix
22 temporal =======================
2 temporal
79 schacon Committing in git-svn.
78 schacon
2 temporal To build and install the C++ Protocol Buffer runtime and the Protocol
2 temporal Buffer compiler (protoc) execute the following:
2 temporal
----
Một lần nữa, nó không hiển thị các commit mà bạn đã thực hiện cục bộ trong Git hoặc đã được đẩy lên Subversion trong thời gian đó.
====== Thông tin Máy chủ SVN
Bạn cũng có thể nhận được cùng loại thông tin mà `svn info` cung cấp cho bạn bằng cách chạy `git svn info`:
[source,console]
----
$ git svn info
Path: .
URL: https://schacon-test.googlecode.com/svn/trunk
Repository Root: https://schacon-test.googlecode.com/svn
Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029
Revision: 87
Node Kind: directory
Schedule: normal
Last Changed Author: schacon
Last Changed Rev: 87
Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)
----
Điều này giống như `blame` và `log` ở chỗ nó chạy ngoại tuyến và chỉ được cập nhật kể từ lần cuối cùng bạn giao tiếp với máy chủ Subversion.
====== Bỏ qua Những gì Subversion Bỏ qua
Nếu bạn sao chép một kho chứa Subversion có các thuộc tính `svn:ignore` được đặt ở bất kỳ đâu, bạn có thể sẽ muốn đặt các tập tin `.gitignore` tương ứng để bạn không vô tình commit các tập tin mà bạn không nên.
`git svn` có hai lệnh để giúp giải quyết vấn đề này.
Đầu tiên là `git svn create-ignore`, lệnh này tự động tạo các tập tin `.gitignore` tương ứng cho bạn để commit tiếp theo của bạn có thể bao gồm chúng.
Lệnh thứ hai là `git svn show-ignore`, lệnh này in ra stdout các dòng bạn cần đặt trong một tập tin `.gitignore` để bạn có thể chuyển hướng đầu ra vào tập tin loại trừ dự án của mình:
[source,console]
----
$ git svn show-ignore > .git/info/exclude
----
Bằng cách đó, bạn không xả rác dự án với các tập tin `.gitignore`.
Đây là một lựa chọn tốt nếu bạn là người dùng Git duy nhất trong một nhóm Subversion, và các đồng đội của bạn không muốn các tập tin `.gitignore` trong dự án.
===== Tóm tắt Git-Svn
Các công cụ `git svn` rất hữu ích nếu bạn bị mắc kẹt với một máy chủ Subversion, hoặc nếu không thì đang ở trong một môi trường phát triển đòi hỏi phải chạy một máy chủ Subversion.
Tuy nhiên, bạn nên coi nó là Git bị tê liệt, nếu không bạn sẽ gặp phải các vấn đề trong quá trình dịch có thể gây nhầm lẫn cho bạn và các cộng tác viên của bạn.
Để tránh rắc rối, hãy cố gắng tuân theo các hướng dẫn sau:
* Giữ một lịch sử Git tuyến tính không chứa các commit trộn được thực hiện bởi `git merge`.
Rebase bất kỳ công việc nào bạn làm bên ngoài nhánh chính của mình trở lại nó; đừng trộn nó vào.
* Đừng thiết lập và cộng tác trên một máy chủ Git riêng biệt.
Có thể có một cái để tăng tốc độ sao chép cho các nhà phát triển mới, nhưng đừng đẩy bất cứ thứ gì lên đó mà không có mục nhập `git-svn-id`.
Bạn thậm chí có thể muốn thêm một móc `pre-receive` kiểm tra mỗi thông điệp commit cho một `git-svn-id` và từ chối các lần đẩy chứa các commit không có nó.
Nếu bạn tuân theo các hướng dẫn đó, làm việc với một máy chủ Subversion có thể dễ chịu hơn.
Tuy nhiên, nếu có thể chuyển sang một máy chủ Git thực sự, làm như vậy có thể mang lại cho nhóm của bạn nhiều hơn nữa.
==== Git và Mercurial
(((Interoperation with other VCSs, Mercurial)))
(((Mercurial)))
Vũ trụ DVCS lớn hơn không chỉ có Git.
Trên thực tế, có nhiều hệ thống khác trong không gian này, mỗi hệ thống có góc nhìn riêng về cách thực hiện kiểm soát phiên bản phân tán một cách chính xác.
Ngoài Git, phổ biến nhất là Mercurial, và cả hai rất giống nhau về nhiều mặt.
Tin tốt, nếu bạn thích hành vi phía máy khách của Git nhưng đang làm việc với một dự án có mã nguồn được kiểm soát bằng Mercurial, là có một cách để sử dụng Git như một máy khách cho một kho chứa được lưu trữ bằng Mercurial.
Vì cách Git nói chuyện với các kho chứa máy chủ là thông qua các máy từ xa (remotes), nên không có gì ngạc nhiên khi cầu nối này được triển khai như một trình trợ giúp từ xa (remote helper).
Tên của dự án là git-remote-hg, và nó có thể được tìm thấy tại https://github.com/felipec/git-remote-hg[^].
===== git-remote-hg
Đầu tiên, bạn cần cài đặt git-remote-hg.
Về cơ bản, điều này đòi hỏi phải thả tập tin của nó vào đâu đó trong đường dẫn của bạn, như sau:
[source,console]
----
$ curl -o ~/bin/git-remote-hg \
https://raw.githubusercontent.com/felipec/git-remote-hg/master/git-remote-hg
$ chmod +x ~/bin/git-remote-hg
----
…giả sử `~/bin` nằm trong `$PATH` của bạn.
Git-remote-hg có một phụ thuộc khác: thư viện `mercurial` cho Python.
Nếu bạn đã cài đặt Python, điều này đơn giản như sau:
[source,console]
----
$ pip install mercurial
----
Nếu bạn chưa cài đặt Python, hãy truy cập https://www.python.org/[^] và tải nó trước.
Điều cuối cùng bạn sẽ cần là máy khách Mercurial.
Truy cập https://www.mercurial-scm.org/[^] và cài đặt nó nếu bạn chưa cài đặt.
Bây giờ bạn đã sẵn sàng để quẩy.
Tất cả những gì bạn cần là một kho chứa Mercurial mà bạn có thể đẩy đến.
May mắn thay, mọi kho chứa Mercurial đều có thể hoạt động theo cách này, vì vậy chúng ta sẽ chỉ sử dụng kho chứa "hello world" mà mọi người sử dụng để học Mercurial:
[source,console]
----
$ hg clone http://selenic.com/repo/hello /tmp/hello
----
===== Bắt đầu
Bây giờ chúng ta đã có một kho chứa "`phía máy chủ`" phù hợp, chúng ta có thể đi qua một quy trình làm việc điển hình.
Như bạn sẽ thấy, hai hệ thống này đủ giống nhau để không có nhiều ma sát.
Như mọi khi với Git, đầu tiên chúng ta sao chép (clone):
[source,console]
----
$ git clone hg::/tmp/hello /tmp/hello-git
$ cd /tmp/hello-git
$ git log --oneline --graph --decorate
* ac7955c (HEAD, origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master, master) Create a makefile
* 65bb417 Create a standard 'hello, world' program
----
Bạn sẽ nhận thấy rằng làm việc với một kho chứa Mercurial sử dụng lệnh `git clone` tiêu chuẩn.
Đó là bởi vì git-remote-hg đang hoạt động ở mức khá thấp, sử dụng một cơ chế tương tự như cách giao thức HTTP/S của Git được triển khai (các trình trợ giúp từ xa).
Vì Git và Mercurial đều được thiết kế để mọi máy khách có một bản sao đầy đủ của lịch sử kho chứa, lệnh này tạo ra một bản sao đầy đủ, bao gồm tất cả lịch sử của dự án, và thực hiện nó khá nhanh chóng.
Lệnh `log` hiển thị hai commit, commit mới nhất được trỏ đến bởi một loạt các tham chiếu.
Hóa ra một số trong số này thực sự không có ở đó.
Hãy xem những gì thực sự có trong thư mục `.git`:
[source,console]
----
$ tree .git/refs
.git/refs
├── heads
│ └── master
├── hg
│ └── origin
│ ├── bookmarks
│ │ └── master
│ └── branches
│ └── default
├── notes
│ └── hg
├── remotes
│ └── origin
│ └── HEAD
└── tags
9 directories, 5 files
----
Git-remote-hg đang cố gắng làm cho mọi thứ trở nên giống Git hơn về mặt thành ngữ, nhưng bên dưới nó đang quản lý ánh xạ khái niệm giữa hai hệ thống hơi khác nhau.
Thư mục `refs/hg` là nơi các tham chiếu từ xa thực tế được lưu trữ.
Ví dụ, `refs/hg/origin/branches/default` là một tập tin tham chiếu Git chứa SHA-1 bắt đầu bằng "`ac7955c`", đó là commit mà `master` trỏ đến.
Vì vậy, thư mục `refs/hg` giống như một `refs/remotes/origin` giả, nhưng nó có sự phân biệt thêm giữa dấu trang (bookmarks) và nhánh (branches).
Tập tin `notes/hg` là điểm bắt đầu cho cách git-remote-hg ánh xạ các hàm băm commit Git sang các ID changeset Mercurial.
Hãy khám phá một chút:
[source,console]
----
$ cat notes/hg
d4c10386...
$ git cat-file -p d4c10386...
tree 1781c96...
author remote-hg <> 1408066400 -0800
committer remote-hg <> 1408066400 -0800
Notes for master
$ git ls-tree 1781c96...
100644 blob ac9117f... 65bb417...
100644 blob 485e178... ac7955c...
$ git cat-file -p ac9117f
0a04b987be5ae354b710cefeba0e2d9de7ad41a9
----
Vì vậy `refs/notes/hg` trỏ đến một cây (tree), trong cơ sở dữ liệu đối tượng Git là một danh sách các đối tượng khác có tên.
`git ls-tree` xuất ra chế độ, loại, hàm băm đối tượng, và tên tập tin cho các mục bên trong một cây.
Khi chúng ta đào sâu xuống một trong các mục cây, chúng ta thấy rằng bên trong nó là một blob có tên "`ac9117f`" (hàm băm SHA-1 của commit được trỏ đến bởi `master`), với nội dung "`0a04b98`" (là ID của changeset Mercurial tại đỉnh của nhánh `default`).
Tin tốt là chúng ta hầu như không phải lo lắng về tất cả những điều này.
Quy trình làm việc điển hình sẽ không khác nhiều so với làm việc với một máy từ xa Git.
Có một điều nữa chúng ta nên chú ý trước khi tiếp tục: bỏ qua (ignores).
Mercurial và Git sử dụng cơ chế rất giống nhau cho việc này, nhưng có khả năng bạn không muốn thực sự commit một tập tin `.gitignore` vào một kho chứa Mercurial.
May mắn thay, Git có một cách để bỏ qua các tập tin cục bộ đối với một kho chứa trên đĩa, và định dạng Mercurial tương thích với Git, vì vậy bạn chỉ cần sao chép nó qua:
[source,console]
----
$ cp .hgignore .git/info/exclude
----
Tập tin `.git/info/exclude` hoạt động giống như một `.gitignore`, nhưng không được bao gồm trong các commit.
===== Quy trình làm việc
Giả sử chúng ta đã thực hiện một số công việc và thực hiện một số commit trên nhánh `master`, và bạn đã sẵn sàng đẩy nó lên kho chứa từ xa.
Đây là những gì kho chứa của chúng ta trông giống như ngay bây giờ:
[source,console]
----
$ git log --oneline --graph --decorate
* ba04a2a (HEAD, master) Update makefile
* d25d16f Goodbye
* ac7955c (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Create a makefile
* 65bb417 Create a standard 'hello, world' program
----
Nhánh `master` của chúng ta đi trước `origin/master` hai commit, nhưng hai commit đó chỉ tồn tại trên máy cục bộ của chúng ta.
Hãy xem liệu có ai khác đã và đang làm công việc quan trọng cùng lúc không:
[source,console]
----
$ git fetch
From hg::/tmp/hello
ac7955c..df85e87 master -> origin/master
ac7955c..df85e87 branches/default -> origin/branches/default
$ git log --oneline --graph --decorate --all
* 7b07969 (refs/notes/hg) Notes for default
* d4c1038 Notes for master
* df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
| * ba04a2a (HEAD, master) Update makefile
| * d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard 'hello, world' program
----
Vì chúng ta đã sử dụng cờ `--all`, chúng ta thấy các tham chiếu "`notes`" được sử dụng nội bộ bởi git-remote-hg, nhưng chúng ta có thể bỏ qua chúng.
Phần còn lại là những gì chúng ta mong đợi; `origin/master` đã tiến thêm một commit, và lịch sử của chúng ta hiện đã phân kỳ.
Không giống như các hệ thống khác chúng ta làm việc trong chương này, Mercurial có khả năng xử lý các lần trộn, vì vậy chúng ta sẽ không làm bất cứ điều gì cầu kỳ.
[source,console]
----
$ git merge origin/master
Auto-merging hello.c
Merge made by the 'recursive' strategy.
hello.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --decorate
* 0c64627 (HEAD, master) Merge remote-tracking branch 'origin/master'
|\
| * df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
* | ba04a2a Update makefile
* | d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard 'hello, world' program
----
Hoàn hảo.
Chúng ta chạy các bài kiểm tra và mọi thứ đều vượt qua, vì vậy chúng ta đã sẵn sàng chia sẻ công việc của mình với phần còn lại của nhóm:
[source,console]
----
$ git push
To hg::/tmp/hello
df85e87..0c64627 master -> master
----
Thế là xong!
Nếu bạn xem xét kho chứa Mercurial, bạn sẽ thấy rằng điều này đã làm những gì chúng ta mong đợi:
[source,console]
----
$ hg log -G --style compact
o 5[tip]:4,2 dc8fa4f932b8 2014-08-14 19:33 -0700 ben
|\ Merge remote-tracking branch 'origin/master'
| |
| o 4 64f27bcefc35 2014-08-14 19:27 -0700 ben
| | Update makefile
| |
| o 3:1 4256fc29598f 2014-08-14 19:27 -0700 ben
| | Goodbye
| |
@ | 2 7db0b4848b3c 2014-08-14 19:30 -0700 ben
|/ Add some documentation
|
o 1 82e55d328c8c 2005-08-26 01:21 -0700 mpm
| Create a makefile
|
o 0 0a04b987be5a 2005-08-26 01:20 -0700 mpm
Create a standard 'hello, world' program
----
Changeset được đánh số _2_ được thực hiện bởi Mercurial, và các changeset được đánh số _3_ và _4_ được thực hiện bởi git-remote-hg, bằng cách đẩy các commit được thực hiện bằng Git.
===== Nhánh và Dấu trang
Git chỉ có một loại nhánh: một tham chiếu di chuyển khi các commit được thực hiện.
Trong Mercurial, loại tham chiếu này được gọi là một "`dấu trang`" (bookmark), và nó hoạt động theo cách tương tự như một nhánh Git.
Khái niệm về một "`nhánh`" của Mercurial nặng nề hơn.
Nhánh mà một changeset được thực hiện trên đó được ghi lại _cùng với changeset_, điều đó có nghĩa là nó sẽ luôn nằm trong lịch sử kho chứa.
Dưới đây là một ví dụ về một commit được thực hiện trên nhánh `develop`:
[source,console]
----
$ hg log -l 1
changeset: 6:8f65e5e02793
branch: develop
tag: tip
user: Ben Straub <ben@straub.cc>
date: Thu Aug 14 20:06:38 2014 -0700
summary: More documentation
----
Lưu ý dòng bắt đầu bằng "`branch`".
Git không thể thực sự sao chép điều này (và không cần phải làm vậy; cả hai loại nhánh đều có thể được biểu diễn dưới dạng tham chiếu Git), nhưng git-remote-hg cần hiểu sự khác biệt, bởi vì Mercurial quan tâm.
Tạo các dấu trang Mercurial cũng dễ dàng như tạo các nhánh Git.
Ở phía Git:
[source,console]
----
$ git checkout -b featureA
Switched to a new branch 'featureA'
$ git push origin featureA
To hg::/tmp/hello
* [new branch] featureA -> featureA
----
Chỉ có vậy thôi.
Ở phía Mercurial, nó trông như thế này:
[source,console]
----
$ hg bookmarks
featureA 5:bd5ac26f11f9
$ hg log --style compact -G
@ 6[tip] 8f65e5e02793 2014-08-14 20:06 -0700 ben
| More documentation
|
o 5[featureA]:4,2 bd5ac26f11f9 2014-08-14 20:02 -0700 ben
|\ Merge remote-tracking branch 'origin/master'
| |
| o 4 0434aaa6b91f 2014-08-14 20:01 -0700 ben
| | update makefile
| |
| o 3:1 318914536c86 2014-08-14 20:00 -0700 ben
| | goodbye
| |
o | 2 f098c7f45c4f 2014-08-14 20:01 -0700 ben
|/ Add some documentation
|
o 1 82e55d328c8c 2005-08-26 01:21 -0700 mpm
| Create a makefile
|
o 0 0a04b987be5a 2005-08-26 01:20 -0700 mpm
Create a standard 'hello, world' program
----
Lưu ý thẻ `[featureA]` mới trên bản sửa đổi 5.
Những thứ này hoạt động chính xác như các nhánh Git ở phía Git, với một ngoại lệ: bạn không thể xóa một dấu trang từ phía Git (đây là một hạn chế của các trình trợ giúp từ xa).
Bạn cũng có thể làm việc trên một nhánh Mercurial "`hạng nặng`": chỉ cần đặt một nhánh trong không gian tên `branches`:
[source,console]
----
$ git checkout -b branches/permanent
Switched to a new branch 'branches/permanent'
$ vi Makefile
$ git commit -am 'A permanent change'
$ git push origin branches/permanent
To hg::/tmp/hello
* [new branch] branches/permanent -> branches/permanent
----
Đây là những gì nó trông giống như ở phía Mercurial:
[source,console]
----
$ hg branches
permanent 7:a4529d07aad4
develop 6:8f65e5e02793
default 5:bd5ac26f11f9 (inactive)
$ hg log -G
o changeset: 7:a4529d07aad4
| branch: permanent
| tag: tip
| parent: 5:bd5ac26f11f9
| user: Ben Straub <ben@straub.cc>
| date: Thu Aug 14 20:21:09 2014 -0700
| summary: A permanent change
|
| @ changeset: 6:8f65e5e02793
|/ branch: develop
| user: Ben Straub <ben@straub.cc>
| date: Thu Aug 14 20:06:38 2014 -0700
| summary: More documentation
|
o changeset: 5:bd5ac26f11f9
|\ bookmark: featureA
| | parent: 4:0434aaa6b91f
| | parent: 2:f098c7f45c4f
| | user: Ben Straub <ben@straub.cc>
| | date: Thu Aug 14 20:02:21 2014 -0700
| | summary: Merge remote-tracking branch 'origin/master'
[...]
----
Tên nhánh "`permanent`" đã được ghi lại với changeset được đánh dấu _7_.
Từ phía Git, làm việc với một trong hai kiểu nhánh này là giống nhau: chỉ cần checkout, commit, fetch, merge, pull, và push như bạn thường làm.
Một điều bạn nên biết là Mercurial không hỗ trợ viết lại lịch sử, chỉ thêm vào nó.
Đây là những gì kho chứa Mercurial của chúng ta trông giống như sau một lần rebase tương tác và một lần đẩy bắt buộc (force-push):
[source,console]
----
$ hg log --style compact -G
o 10[tip] 99611176cbc9 2014-08-14 20:21 -0700 ben
| A permanent change
|
o 9 f23e12f939c3 2014-08-14 20:01 -0700 ben
| Add some documentation
|
o 8:1 c16971d33922 2014-08-14 20:00 -0700 ben
| goodbye
|
| o 7:5 a4529d07aad4 2014-08-14 20:21 -0700 ben
| | A permanent change
| |
| | @ 6 8f65e5e02793 2014-08-14 20:06 -0700 ben
| |/ More documentation
| |
| o 5[featureA]:4,2 bd5ac26f11f9 2014-08-14 20:02 -0700 ben
| |\ Merge remote-tracking branch 'origin/master'
| | |
| | o 4 0434aaa6b91f 2014-08-14 20:01 -0700 ben
| | | update makefile
| | |
| | +---o 3:1 318914536c86 2014-08-14 20:00 -0700 ben
| | goodbye
| |
| o 2 f098c7f45c4f 2014-08-14 20:01 -0700 ben
|/ Add some documentation
|
o 1 82e55d328c8c 2005-08-26 01:21 -0700 mpm
| Create a makefile
|
o 0 0a04b987be5a 2005-08-26 01:20 -0700 mpm
Create a standard "hello, world" program
----
Các changeset _8_, _9_, và _10_ đã được tạo và thuộc về nhánh `permanent`, nhưng các changeset cũ vẫn còn đó.
Điều này có thể *rất* khó hiểu đối với các đồng đội của bạn, những người đang sử dụng Mercurial, vì vậy hãy cố gắng tránh nó.
===== Tóm tắt Mercurial
Git và Mercurial đủ giống nhau để làm việc qua ranh giới là khá không đau đớn.
Nếu bạn tránh thay đổi lịch sử đã rời khỏi máy của mình (như thường được khuyến nghị), bạn thậm chí có thể không biết rằng đầu bên kia là Mercurial.
==== Git và Perforce
(((Interoperation with other VCSs, Perforce)))
(((Perforce)))
Perforce là một hệ thống kiểm soát phiên bản rất phổ biến trong môi trường doanh nghiệp.
Nó đã tồn tại từ năm 1995, điều này làm cho nó trở thành hệ thống lâu đời nhất được đề cập trong chương này.
Như vậy, nó được thiết kế với các ràng buộc của thời đại của nó; nó giả định rằng bạn luôn được kết nối với một máy chủ trung tâm duy nhất, và chỉ một phiên bản được giữ trên đĩa cục bộ.
Chắc chắn, các tính năng và ràng buộc của nó rất phù hợp với một số vấn đề cụ thể, nhưng có rất nhiều dự án sử dụng Perforce mà Git thực sự sẽ hoạt động tốt hơn.
Có hai tùy chọn nếu bạn muốn kết hợp việc sử dụng Perforce và Git.
Tùy chọn đầu tiên chúng tôi sẽ đề cập là cầu nối "`Git Fusion`" từ các nhà sản xuất Perforce, cho phép bạn hiển thị các cây con của kho lưu trữ Perforce của bạn dưới dạng các kho chứa Git đọc-ghi.
Tùy chọn thứ hai là git-p4, một cầu nối phía máy khách cho phép bạn sử dụng Git như một máy khách Perforce, mà không yêu cầu bất kỳ cấu hình lại nào của máy chủ Perforce.
[[_p4_git_fusion]]
===== Git Fusion
(((Perforce, Git Fusion)))
Perforce cung cấp một sản phẩm gọi là Git Fusion (có sẵn tại https://www.perforce.com/manuals/git-fusion/[^]), đồng bộ hóa một máy chủ Perforce với các kho chứa Git ở phía máy chủ.
====== Thiết lập
Đối với các ví dụ của chúng tôi, chúng tôi sẽ sử dụng phương pháp cài đặt dễ nhất cho Git Fusion, đó là tải xuống một máy ảo chạy daemon Perforce và Git Fusion.
Bạn có thể lấy hình ảnh máy ảo từ https://www.perforce.com/downloads[^], và khi nó tải xuống xong, hãy nhập nó vào phần mềm ảo hóa yêu thích của bạn (chúng tôi sẽ sử dụng VirtualBox).
Khi khởi động máy lần đầu tiên, nó yêu cầu bạn tùy chỉnh mật khẩu cho ba người dùng Linux (`root`, `perforce`, và `git`), và cung cấp một tên phiên bản, có thể được sử dụng để phân biệt cài đặt này với các cài đặt khác trên cùng một mạng.
Khi tất cả đã hoàn tất, bạn sẽ thấy điều này:
.Màn hình khởi động máy ảo Git Fusion
image::images/git-fusion-boot.png[Màn hình khởi động máy ảo Git Fusion]
Bạn nên ghi chú địa chỉ IP được hiển thị ở đây, chúng ta sẽ sử dụng nó sau này.
Tiếp theo, chúng ta sẽ tạo một người dùng Perforce.
Chọn tùy chọn "`Login`" ở dưới cùng và nhấn enter (hoặc SSH vào máy), và đăng nhập với tư cách `root`.
Sau đó sử dụng các lệnh này để tạo một người dùng:
[source,console]
----
$ p4 -p localhost:1666 -u super user -f john
$ p4 -p localhost:1666 -u john passwd
$ exit
----
Lệnh đầu tiên sẽ mở trình soạn thảo VI để tùy chỉnh người dùng, nhưng bạn có thể chấp nhận các giá trị mặc định bằng cách gõ `:wq` và nhấn enter.
Lệnh thứ hai sẽ nhắc bạn nhập mật khẩu hai lần.
Đó là tất cả những gì chúng ta cần làm với dấu nhắc shell, vì vậy hãy thoát khỏi phiên.
Điều tiếp theo bạn sẽ cần làm để theo dõi là bảo Git không xác minh chứng chỉ SSL.
Hình ảnh Git Fusion đi kèm với một chứng chỉ, nhưng nó dành cho một tên miền sẽ không khớp với địa chỉ IP của máy ảo của bạn, vì vậy Git sẽ từ chối kết nối HTTPS.
Nếu đây sẽ là một cài đặt vĩnh viễn, hãy tham khảo hướng dẫn sử dụng Perforce Git Fusion để cài đặt một chứng chỉ khác; cho mục đích ví dụ của chúng tôi, điều này sẽ đủ:
[source,console]
----
$ export GIT_SSL_NO_VERIFY=true
----
Bây giờ chúng ta có thể kiểm tra xem mọi thứ có hoạt động không.
[source,console]
----
$ git clone https://10.0.1.254/Talkhouse
Cloning into 'Talkhouse'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 630, done.
remote: Compressing objects: 100% (581/581), done.
remote: Total 630 (delta 172), reused 0 (delta 0)
Receiving objects: 100% (630/630), 1.22 MiB | 0 bytes/s, done.
Resolving deltas: 100% (172/172), done.
Checking connectivity... done.
----
Hình ảnh máy ảo đi kèm với một dự án mẫu mà bạn có thể sao chép.
Ở đây chúng ta đang sao chép qua HTTPS, với người dùng `john` mà chúng ta đã tạo ở trên; Git yêu cầu thông tin xác thực cho kết nối này, nhưng bộ nhớ cache thông tin xác thực sẽ cho phép chúng ta bỏ qua bước này cho bất kỳ yêu cầu tiếp theo nào.
====== Cấu hình Fusion
Khi bạn đã cài đặt Git Fusion, bạn sẽ muốn điều chỉnh cấu hình.
Điều này thực sự khá dễ dàng để thực hiện bằng cách sử dụng máy khách Perforce yêu thích của bạn; chỉ cần ánh xạ thư mục `//.git-fusion` trên máy chủ Perforce vào không gian làm việc của bạn.
Cấu trúc tập tin trông như thế này:
[source,console]
----
$ tree
.
├── objects
│ ├── repos
│ │ └── [...]
│ └── trees
│ └── [...]
│
├── p4gf_config
├── repos
│ └── Talkhouse
│ └── p4gf_config
└── users
└── p4gf_usermap
498 directories, 287 files
----
Thư mục `objects` được sử dụng nội bộ bởi Git Fusion để ánh xạ các đối tượng Perforce sang Git và ngược lại, bạn sẽ không phải làm gì với bất cứ thứ gì ở đó.
Có một tập tin `p4gf_config` toàn cục trong thư mục này, cũng như một tập tin cho mỗi kho chứa – đây là các tập tin cấu hình xác định cách Git Fusion hoạt động.
Hãy xem xét tập tin trong thư mục gốc:
[source,ini]
----
[repo-creation]
charset = utf8
[git-to-perforce]
change-owner = author
enable-git-branch-creation = yes
enable-swarm-reviews = yes
enable-git-merge-commits = yes
enable-git-submodules = yes
preflight-commit = none
ignore-author-permissions = no
read-permission-check = none
git-merge-avoidance-after-change-num = 12107
[perforce-to-git]
http-url = none
ssh-url = none
[@features]
imports = False
chunked-push = False
matrix2 = False
parallel-push = False
[authentication]
email-case-sensitivity = no
----
Chúng tôi sẽ không đi vào ý nghĩa của các cờ này ở đây, nhưng lưu ý rằng đây chỉ là một tập tin văn bản được định dạng INI, giống như Git sử dụng cho cấu hình.
Tập tin này chỉ định các tùy chọn toàn cục, sau đó có thể bị ghi đè bởi các tập tin cấu hình cụ thể cho kho chứa, như `repos/Talkhouse/p4gf_config`.
Nếu bạn mở tập tin này, bạn sẽ thấy một phần `[@repo]` với một số thiết lập khác với các giá trị mặc định toàn cục.
Bạn cũng sẽ thấy các phần trông giống như thế này:
[source,ini]
----
[Talkhouse-master]
git-branch-name = master
view = //depot/Talkhouse/main-dev/... ...
----
Đây là một ánh xạ giữa một nhánh Perforce và một nhánh Git.
Phần này có thể được đặt tên bất cứ thứ gì bạn thích, miễn là tên là duy nhất.
`git-branch-name` cho phép bạn chuyển đổi một đường dẫn depot sẽ rườm rà dưới Git thành một tên thân thiện hơn.
Thiết lập `view` kiểm soát cách các tập tin Perforce được ánh xạ vào kho chứa Git, sử dụng cú pháp ánh xạ view tiêu chuẩn.
Có thể chỉ định nhiều hơn một ánh xạ, như trong ví dụ này:
[source,ini]
----
[multi-project-mapping]
git-branch-name = master
view = //depot/project1/main/... project1/...
//depot/project2/mainline/... project2/...
----
Bằng cách này, nếu ánh xạ không gian làm việc bình thường của bạn bao gồm các thay đổi trong cấu trúc của các thư mục, bạn có thể sao chép điều đó với một kho chứa Git.
Tập tin cuối cùng chúng ta sẽ thảo luận là `users/p4gf_usermap`, ánh xạ người dùng Perforce sang người dùng Git, và bạn thậm chí có thể không cần.
Khi chuyển đổi từ một changeset Perforce sang một commit Git, hành vi mặc định của Git Fusion là tra cứu người dùng Perforce, và sử dụng địa chỉ email và tên đầy đủ được lưu trữ ở đó cho trường tác giả/người commit trong Git.
Khi chuyển đổi theo cách khác, mặc định là tra cứu người dùng Perforce với địa chỉ email được lưu trữ trong trường tác giả của commit Git, và gửi changeset với tư cách là người dùng đó (với các quyền được áp dụng).
Trong hầu hết các trường hợp, hành vi này sẽ hoạt động tốt, nhưng hãy xem xét tập tin ánh xạ sau:
[source]
----
john john@example.com "John Doe"
john johnny@appleseed.net "John Doe"
bob employeeX@example.com "Anon X. Mouse"
joe employeeY@example.com "Anon Y. Mouse"
----
Mỗi dòng có định dạng `<user> <email> "<full name>"`, và tạo một ánh xạ người dùng duy nhất.
Hai dòng đầu tiên ánh xạ hai địa chỉ email riêng biệt vào cùng một tài khoản người dùng Perforce.
Điều này hữu ích nếu bạn đã tạo các commit Git dưới một số địa chỉ email khác nhau (hoặc thay đổi địa chỉ email), nhưng muốn chúng được ánh xạ vào cùng một người dùng Perforce.
Khi tạo một commit Git từ một changeset Perforce, dòng đầu tiên khớp với người dùng Perforce được sử dụng cho thông tin tác giả Git.
Hai dòng cuối cùng che giấu tên thực và địa chỉ email của Bob và Joe khỏi các commit Git được tạo.
Điều này tốt nếu bạn muốn mở mã nguồn một dự án nội bộ, nhưng không muốn xuất bản thư mục nhân viên của bạn ra toàn thế giới.
Lưu ý rằng các địa chỉ email và tên đầy đủ nên là duy nhất, trừ khi bạn muốn tất cả các commit Git được gán cho một tác giả hư cấu duy nhất.
====== Quy trình làm việc
Perforce Git Fusion là một cầu nối hai chiều giữa Perforce và kiểm soát phiên bản Git.
Hãy xem xét cảm giác làm việc từ phía Git như thế nào.
Chúng ta sẽ giả định rằng chúng ta đã ánh xạ trong dự án "`Jam`" bằng cách sử dụng một tập tin cấu hình như được hiển thị ở trên, mà chúng ta có thể sao chép như thế này:
[source,console]
----
$ git clone https://10.0.1.254/Jam
Cloning into 'Jam'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 2070, done.
remote: Compressing objects: 100% (1704/1704), done.
Receiving objects: 100% (2070/2070), 1.21 MiB | 0 bytes/s, done.
remote: Total 2070 (delta 1242), reused 0 (delta 0)
Resolving deltas: 100% (1242/1242), done.
Checking connectivity... done.
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
remotes/origin/rel2.1
$ git log --oneline --decorate --graph --all
* 0a38c33 (origin/rel2.1) Create Jam 2.1 release branch.
| * d254865 (HEAD, origin/master, origin/HEAD, master) Upgrade to latest metrowerks on Beos -- the Intel one.
| * bd2f54a Put in fix for jam's NT handle leak.
| * c0f29e7 Fix URL in a jam doc
| * cc644ac Radstone's lynx port.
[...]
----
Lần đầu tiên bạn làm điều này, nó có thể mất một chút thời gian.
Điều đang xảy ra là Git Fusion đang chuyển đổi tất cả các changeset áp dụng trong lịch sử Perforce thành các commit Git.
Điều này xảy ra cục bộ trên máy chủ, vì vậy nó tương đối nhanh, nhưng nếu bạn có nhiều lịch sử, nó vẫn có thể mất một chút thời gian.
Các lần lấy tiếp theo thực hiện chuyển đổi gia tăng, vì vậy nó sẽ cảm thấy giống như tốc độ bản địa của Git hơn.
Như bạn có thể thấy, kho chứa của chúng ta trông giống hệt như bất kỳ kho chứa Git nào khác mà bạn có thể làm việc.
Có ba nhánh, và Git đã hữu ích tạo một nhánh `master` cục bộ theo dõi `origin/master`.
Hãy thực hiện một chút công việc, và tạo một vài commit mới:
[source,console]
----
# ...
$ git log --oneline --decorate --graph --all
* cfd46ab (HEAD, master) Add documentation for new feature
* a730d77 Whitespace
* d254865 (origin/master, origin/HEAD) Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]
----
Chúng ta có hai commit mới.
Bây giờ hãy kiểm tra xem có ai khác đã làm việc không:
[source,console]
----
$ git fetch
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://10.0.1.254/Jam
d254865..6afeb15 master -> origin/master
$ git log --oneline --decorate --graph --all
* 6afeb15 (origin/master, origin/HEAD) Update copyright
| * cfd46ab (HEAD, master) Add documentation for new feature
| * a730d77 Whitespace
|/
* d254865 Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]
----
Có vẻ như có người đã làm!
Bạn sẽ không biết điều đó từ góc nhìn này, nhưng commit `6afeb15` thực sự đã được tạo bằng cách sử dụng một máy khách Perforce.
Nó chỉ trông giống như một commit khác từ quan điểm của Git, đó chính xác là điểm mấu chốt.
Hãy xem máy chủ Perforce xử lý một commit trộn như thế nào:
[source,console]
----
$ git merge origin/master
Auto-merging README
Merge made by the 'recursive' strategy.
README | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Perforce: 100% (3/3) Loading commit tree into memory...
remote: Perforce: 100% (5/5) Finding child commits...
remote: Perforce: Running git fast-export...
remote: Perforce: 100% (3/3) Checking commits...
remote: Processing will continue even if connection is closed.
remote: Perforce: 100% (3/3) Copying changelists...
remote: Perforce: Submitting new Git commit objects to Perforce: 4
To https://10.0.1.254/Jam
6afeb15..89cba2b master -> master
----
Git nghĩ rằng nó đã hoạt động.
Hãy xem lịch sử của tập tin `README` từ quan điểm của Perforce, sử dụng tính năng đồ thị bản sửa đổi của `p4v`:
.Đồ thị bản sửa đổi Perforce kết quả từ Git push
image::images/git-fusion-perforce-graph.png[Đồ thị bản sửa đổi Perforce kết quả từ Git push]
Nếu bạn chưa bao giờ thấy góc nhìn này trước đây, nó có thể có vẻ khó hiểu, nhưng nó hiển thị các khái niệm giống như một trình xem đồ họa cho lịch sử Git.
Chúng ta đang xem xét lịch sử của tập tin `README`, vì vậy cây thư mục ở trên cùng bên trái chỉ hiển thị tập tin đó khi nó xuất hiện trong các nhánh khác nhau.
Ở trên cùng bên phải, chúng ta có một đồ thị trực quan về cách các bản sửa đổi khác nhau của tập tin có liên quan, và góc nhìn toàn cảnh của đồ thị này ở dưới cùng bên phải.
Phần còn lại của góc nhìn được dành cho góc nhìn chi tiết cho bản sửa đổi được chọn (`2` trong trường hợp này).
Một điều cần lưu ý là đồ thị trông giống hệt như đồ thị trong lịch sử của Git.
Perforce không có một nhánh được đặt tên để lưu trữ các commit `1` và `2`, vì vậy nó đã tạo một nhánh "`ẩn danh`" trong thư mục `.git-fusion` để giữ nó.
Điều này cũng sẽ xảy ra đối với các nhánh Git được đặt tên không tương ứng với một nhánh Perforce được đặt tên (và sau đó bạn có thể ánh xạ chúng vào một nhánh Perforce bằng cách sử dụng tập tin cấu hình).
Hầu hết điều này xảy ra đằng sau hậu trường, nhưng kết quả cuối cùng là một người trong nhóm có thể đang sử dụng Git, một người khác có thể đang sử dụng Perforce, và không ai trong số họ sẽ biết về sự lựa chọn của người kia.
====== Tóm tắt Git-Fusion
Nếu bạn có (hoặc có thể có) quyền truy cập vào máy chủ Perforce của mình, Git Fusion là một cách tuyệt vời để làm cho Git và Perforce nói chuyện với nhau.
Có một chút cấu hình liên quan, nhưng đường cong học tập không quá dốc.
Đây là một trong số ít phần trong chương này mà các cảnh báo về việc sử dụng toàn bộ sức mạnh của Git sẽ không xuất hiện.
Điều đó không có nghĩa là Perforce sẽ hài lòng với mọi thứ bạn ném vào nó – nếu bạn cố gắng viết lại lịch sử đã được đẩy, Git Fusion sẽ từ chối nó – nhưng Git Fusion cố gắng rất nhiều để cảm thấy bản địa.
Bạn thậm chí có thể sử dụng các submodule Git (mặc dù chúng sẽ trông lạ đối với người dùng Perforce), và trộn các nhánh (điều này sẽ được ghi lại như một tích hợp ở phía Perforce).
Nếu bạn không thể thuyết phục quản trị viên máy chủ của mình thiết lập Git Fusion, vẫn có một cách để sử dụng các công cụ này cùng nhau.
[[_git_p4_client]]
===== Git-p4
(((git commands, p4)))
Git-p4 là một cầu nối hai chiều giữa Git và Perforce.
Nó chạy hoàn toàn bên trong kho chứa Git của bạn, vì vậy bạn sẽ không cần bất kỳ loại quyền truy cập nào vào máy chủ Perforce (ngoài thông tin xác thực người dùng, tất nhiên).
Git-p4 không phải là một giải pháp linh hoạt hoặc hoàn chỉnh như Git Fusion, nhưng nó cho phép bạn làm hầu hết những gì bạn muốn làm mà không xâm phạm vào môi trường máy chủ.
[NOTE]
======
Bạn sẽ cần công cụ `p4` ở đâu đó trong `PATH` của bạn để làm việc với git-p4.
Tính đến thời điểm viết bài này, nó có sẵn miễn phí tại https://www.perforce.com/downloads/helix-command-line-client-p4[^].
======
====== Thiết lập
Cho mục đích ví dụ, chúng ta sẽ chạy máy chủ Perforce từ Git Fusion OVA như được hiển thị ở trên, nhưng chúng ta sẽ bỏ qua máy chủ Git Fusion và đi trực tiếp đến kiểm soát phiên bản Perforce.
Để sử dụng máy khách dòng lệnh `p4` (mà git-p4 phụ thuộc vào), bạn sẽ cần đặt một vài biến môi trường:
[source,console]
----
$ export P4PORT=10.0.1.254:1666
$ export P4USER=john
----
====== Bắt đầu
Như với bất cứ thứ gì trong Git, lệnh đầu tiên là sao chép:
[source,console]
----
$ git p4 clone //depot/www/live www-shallow
Importing from //depot/www/live into www-shallow
Initialized empty Git repository in /private/tmp/www-shallow/.git/
Doing initial import of //depot/www/live/ from revision #head into refs/remotes/p4/master
----
Điều này tạo ra những gì trong thuật ngữ Git là một bản sao "`nông`" (shallow); chỉ bản sửa đổi Perforce mới nhất được nhập vào Git; hãy nhớ rằng, Perforce không được thiết kế để cung cấp mọi bản sửa đổi cho mọi người dùng.
Điều này đủ để sử dụng Git như một máy khách Perforce, nhưng cho các mục đích khác thì không đủ.
Khi nó hoàn thành, chúng ta có một kho chứa Git hoạt động đầy đủ:
[source,console]
----
$ cd myproject
$ git log --oneline --all --graph --decorate
* 70eaf78 (HEAD, p4/master, p4/HEAD, master) Initial import of //depot/www/live/ from the state at revision #head
----
Lưu ý cách có một máy từ xa "`p4`" cho máy chủ Perforce, nhưng mọi thứ khác trông giống như một bản sao tiêu chuẩn.
Thực ra, điều đó hơi gây hiểu lầm; thực sự không có một máy từ xa ở đó.
[source,console]
----
$ git remote -v
----
Không có máy từ xa nào tồn tại trong kho chứa này cả.
Git-p4 đã tạo một số tham chiếu để đại diện cho trạng thái của máy chủ, và chúng trông giống như các tham chiếu từ xa đối với `git log`, nhưng chúng không được quản lý bởi chính Git, và bạn không thể đẩy lên chúng.
====== Quy trình làm việc
Được rồi, hãy thực hiện một số công việc.
Giả sử bạn đã đạt được một số tiến bộ trên một tính năng rất quan trọng, và bạn đã sẵn sàng để hiển thị nó cho phần còn lại của nhóm của bạn.
[source,console]
----
$ git log --oneline --all --graph --decorate
* 018467c (HEAD, master) Change page title
* c0fb617 Update link
* 70eaf78 (p4/master, p4/HEAD) Initial import of //depot/www/live/ from the state at revision #head
----
Chúng ta đã thực hiện hai commit mới mà chúng ta đã sẵn sàng gửi đến máy chủ Perforce.
Hãy kiểm tra xem có ai khác đã làm việc hôm nay không:
[source,console]
----
$ git p4 sync
git p4 sync
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12142 (100%)
$ git log --oneline --all --graph --decorate
* 75cd059 (p4/master, p4/HEAD) Update copyright
| * 018467c (HEAD, master) Change page title
| * c0fb617 Update link
|/
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head
----
Có vẻ như họ đã làm, và `master` và `p4/master` đã phân kỳ.
Hệ thống phân nhánh của Perforce _không giống gì_ như của Git, vì vậy việc gửi các commit trộn không có ý nghĩa gì.
Git-p4 khuyến nghị rằng bạn rebase các commit của mình, và thậm chí đi kèm với một phím tắt để làm như vậy:
[source,console]
----
$ git p4 rebase
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
No changes to import!
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
Applying: Update link
Applying: Change page title
index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
----
Bạn có thể có thể biết từ đầu ra, nhưng `git p4 rebase` là một phím tắt cho `git p4 sync` theo sau là `git rebase p4/master`.
Nó thông minh hơn một chút so với điều đó, đặc biệt là khi làm việc với nhiều nhánh, nhưng đây là một xấp xỉ tốt.
Bây giờ lịch sử của chúng ta lại tuyến tính, và chúng ta đã sẵn sàng đóng góp các thay đổi của mình trở lại Perforce.
Lệnh `git p4 submit` sẽ cố gắng tạo một bản sửa đổi Perforce mới cho mỗi commit Git giữa `p4/master` và `master`.
Chạy nó đưa chúng ta vào trình soạn thảo yêu thích của chúng ta, và nội dung của tập tin trông giống như thế này:
[source,console]
----
# A Perforce Change Specification.
#
# Change: The change number. 'new' on a new changelist.
# Date: The date this specification was last modified.
# Client: The client on which the changelist was created. Read-only.
# User: The user who created the changelist.
# Status: Either 'pending' or 'submitted'. Read-only.
# Type: Either 'public' or 'restricted'. Default is 'public'.
# Description: Comments about the changelist. Required.
# Jobs: What opened jobs are to be closed by this changelist.
# You may delete jobs from this list. (New changelists only.)
# Files: What opened files from the default changelist are to be added
# to this changelist. You may delete files from this list.
# (New changelists only.)
Change: new
Client: john_bens-mbp_8487
User: john
Status: new
Description:
Update link
Files:
//depot/www/live/index.html # edit
######## git author ben@straub.cc does not match your p4 account.
######## Use option --preserve-user to modify authorship.
######## Variable git-p4.skipUserNameCheck hides this message.
######## everything below this line is just the diff #######
--- //depot/www/live/index.html 2014-08-31 18:26:05.000000000 0000
+++ /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/index.html 2014-08-31 18:26:05.000000000 0000
@@ -60,7 +60,7 @@
</td>
<td valign=top>
Source and documentation for
-<a href="http://www.perforce.com/jam/jam.html">
+<a href="jam.html">
Jam/MR</a>,
a software build tool.
</td>
----
Đây chủ yếu là cùng một nội dung mà bạn sẽ thấy bằng cách chạy `p4 submit`, ngoại trừ phần ở cuối mà git-p4 đã hữu ích bao gồm.
Git-p4 cố gắng tôn trọng các thiết lập Git và Perforce của bạn riêng lẻ khi nó phải cung cấp một tên cho một commit hoặc changeset, nhưng trong một số trường hợp bạn muốn ghi đè nó.
Ví dụ, nếu commit Git bạn đang nhập được viết bởi một người đóng góp không có tài khoản người dùng Perforce, bạn vẫn có thể muốn changeset kết quả trông giống như họ đã viết nó (và không phải bạn).
Git-p4 đã hữu ích nhập thông điệp từ commit Git làm nội dung cho changeset Perforce này, vì vậy tất cả những gì chúng ta phải làm là lưu và thoát, hai lần (một lần cho mỗi commit).
Đầu ra shell kết quả sẽ trông giống như thế này:
[source,console]
----
$ git p4 submit
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Synchronizing p4 checkout...
... - file(s) up-to-date.
Applying dbac45b Update link
//depot/www/live/index.html#4 - opened for edit
Change 12143 created with 1 open file(s).
Submitting change 12143.
Locking 1 files ...
edit //depot/www/live/index.html#5
Change 12143 submitted.
Applying 905ec6a Change page title
//depot/www/live/index.html#5 - opened for edit
Change 12144 created with 1 open file(s).
Submitting change 12144.
Locking 1 files ...
edit //depot/www/live/index.html#6
Change 12144 submitted.
All commits applied!
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12144 (100%)
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
$ git log --oneline --all --graph --decorate
* 775a46f (HEAD, p4/master, p4/HEAD, master) Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head
----
Kết quả là như thể chúng ta vừa thực hiện một `git push`, đó là sự tương tự gần nhất với những gì thực sự đã xảy ra.
Lưu ý rằng trong quá trình này, mỗi commit Git được biến thành một changeset Perforce; nếu bạn muốn squash chúng xuống thành một changeset duy nhất, bạn có thể làm điều đó với một rebase tương tác trước khi chạy `git p4 submit`.
Cũng lưu ý rằng các hàm băm SHA-1 của tất cả các commit đã được gửi dưới dạng changeset đã thay đổi; điều này là do git-p4 thêm một dòng vào cuối mỗi commit mà nó chuyển đổi:
[source,console]
----
$ git log -1
commit 775a46f630d8b46535fc9983cf3ebe6b9aa53145
Author: John Doe <john@example.com>
Date: Sun Aug 31 10:31:44 2014 -0800
Change page title
[git-p4: depot-paths = "//depot/www/live/": change = 12144]
----
Điều gì xảy ra nếu bạn cố gắng gửi một commit trộn?
Hãy thử xem.
Đây là tình huống chúng ta đã tự đưa mình vào:
[source,console]
----
$ git log --oneline --all --graph --decorate
* 3be6fd8 (HEAD, master) Correct email address
* 1dcbf21 Merge remote-tracking branch 'p4/master'
|\
| * c4689fc (p4/master, p4/HEAD) Grammar fix
* | cbacd0a Table borders: yes please
* | b4959b6 Trademark
|/
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head
----
Lịch sử Git và Perforce phân kỳ sau `775a46f`.
Phía Git có hai commit, sau đó là một commit trộn với đầu Perforce, sau đó là một commit khác.
Chúng ta sẽ cố gắng gửi những cái này lên trên một changeset duy nhất ở phía Perforce.
Hãy xem điều gì sẽ xảy ra nếu chúng ta cố gắng gửi bây giờ:
[source,console]
----
$ git p4 submit -n
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would synchronize p4 checkout in /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would apply
b4959b6 Trademark
cbacd0a Table borders: yes please
3be6fd8 Correct email address
----
Cờ `-n` là viết tắt của `--dry-run`, cố gắng báo cáo những gì sẽ xảy ra nếu lệnh submit được chạy thực sự.
Trong trường hợp này, có vẻ như chúng ta sẽ tạo ba changeset Perforce, tương ứng với ba commit không trộn chưa tồn tại trên máy chủ Perforce.
Điều đó nghe giống như chính xác những gì chúng ta muốn, hãy xem nó hóa ra như thế nào:
[source,console]
----
$ git p4 submit
[…]
$ git log --oneline --all --graph --decorate
* dadbd89 (HEAD, p4/master, p4/HEAD, master) Correct email address
* 1b79a80 Table borders: yes please
* 0097235 Trademark
* c4689fc Grammar fix
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head
----
Lịch sử của chúng ta đã trở nên tuyến tính, giống như thể chúng ta đã rebase trước khi gửi (đó thực sự chính xác là những gì đã xảy ra).
Điều này có nghĩa là bạn có thể tự do tạo, làm việc, vứt bỏ, và trộn các nhánh ở phía Git mà không sợ rằng lịch sử của bạn sẽ bằng cách nào đó trở nên không tương thích với Perforce.
Nếu bạn có thể rebase nó, bạn có thể đóng góp nó cho một máy chủ Perforce.
[[_git_p4_branches]]
====== Phân nhánh
Nếu dự án Perforce của bạn có nhiều nhánh, bạn không hết may mắn; git-p4 có thể xử lý điều đó theo cách làm cho nó cảm thấy giống như Git.
Giả sử depot Perforce của bạn được bố trí như thế này:
[source]
----
//depot
└── project
├── main
└── dev
----
Và giả sử bạn có một nhánh `dev`, có một đặc tả view trông giống như thế này:
[source]
----
//depot/project/main/... //depot/project/dev/...
----
Git-p4 có thể tự động phát hiện tình huống đó và làm điều đúng đắn:
[source,console]
----
$ git p4 clone --detect-branches //depot/project@all
Importing from //depot/project@all into project
Initialized empty Git repository in /private/tmp/project/.git/
Importing revision 20 (50%)
Importing new branch project/dev
Resuming with change 20
Importing revision 22 (100%)
Updated branches: main dev
$ cd project; git log --oneline --all --graph --decorate
* eae77ae (HEAD, p4/master, p4/HEAD, master) main
| * 10d55fb (p4/project/dev) dev
| * a43cfae Populate //depot/project/main/... //depot/project/dev/....
|/
* 2b83451 Project init
----
Lưu ý bộ chỉ định "`@all`" trong đường dẫn depot; điều đó bảo git-p4 sao chép không chỉ changeset mới nhất cho cây con đó, mà tất cả các changeset đã từng chạm vào các đường dẫn đó.
Điều này gần hơn với khái niệm sao chép của Git, nhưng nếu bạn đang làm việc trên một dự án có lịch sử dài, nó có thể mất một lúc.
Cờ `--detect-branches` bảo git-p4 sử dụng các đặc tả nhánh của Perforce để ánh xạ các nhánh vào các tham chiếu Git.
Nếu các ánh xạ này không có trên máy chủ Perforce (đó là một cách hoàn toàn hợp lệ để sử dụng Perforce), bạn có thể bảo git-p4 các ánh xạ nhánh là gì, và bạn nhận được cùng một kết quả:
[source,console]
----
$ git init project
Initialized empty Git repository in /tmp/project/.git/
$ cd project
$ git config git-p4.branchList main:dev
$ git clone --detect-branches //depot/project@all .
----
Đặt biến cấu hình `git-p4.branchList` thành `main:dev` bảo git-p4 rằng "`main`" và "`dev`" đều là các nhánh, và cái thứ hai là con của cái đầu tiên.
Nếu bây giờ chúng ta `git checkout -b dev p4/project/dev` và thực hiện một số commit, git-p4 đủ thông minh để nhắm đúng nhánh khi chúng ta thực hiện `git p4 submit`.
Thật không may, git-p4 không thể trộn các bản sao nông và nhiều nhánh; nếu bạn có một dự án khổng lồ và muốn làm việc trên nhiều hơn một nhánh, bạn sẽ phải `git p4 clone` một lần cho mỗi nhánh bạn muốn gửi đến.
Để tạo hoặc tích hợp các nhánh, bạn sẽ phải sử dụng một máy khách Perforce.
Git-p4 chỉ có thể đồng bộ hóa và gửi đến các nhánh hiện có, và nó chỉ có thể làm điều đó một changeset tuyến tính tại một thời điểm.
Nếu bạn trộn hai nhánh trong Git và cố gắng gửi changeset mới, tất cả những gì sẽ được ghi lại là một loạt các thay đổi tập tin; siêu dữ liệu về các nhánh nào liên quan đến việc tích hợp sẽ bị mất.
===== Tóm tắt Git và Perforce
Git-p4 làm cho việc sử dụng quy trình làm việc Git với một máy chủ Perforce trở nên khả thi, và nó khá tốt trong việc đó.
Tuy nhiên, điều quan trọng cần nhớ là Perforce đang phụ trách nguồn, và bạn chỉ đang sử dụng Git để làm việc cục bộ.
Chỉ cần thực sự cẩn thận về việc chia sẻ các commit Git; nếu bạn có một máy từ xa mà người khác sử dụng, đừng đẩy bất kỳ commit nào chưa được gửi đến máy chủ Perforce.
Nếu bạn muốn tự do kết hợp việc sử dụng Perforce và Git như các máy khách để kiểm soát nguồn, và bạn có thể thuyết phục quản trị viên máy chủ cài đặt nó, Git Fusion làm cho việc sử dụng Git trở thành một máy khách kiểm soát phiên bản hạng nhất cho một máy chủ Perforce.
[[_migrating]]
=== Di chuyển sang Git
(((Migrating to Git)))
Nếu bạn có mã nguồn trong VCS khác nhưng quyết định bắt đầu dùng Git, bạn phải di chuyển dự án theo cách nào đó.
Phần này trình bày một số công cụ import cho các hệ thống phổ biến, rồi hướng dẫn cách phát triển bộ import tùy chỉnh của riêng bạn.
Bạn sẽ học cách nhập dữ liệu từ một số hệ thống SCM chuyên nghiệp lớn hơn, vì chúng chiếm đa số người chuyển đổi và vì các công cụ chất lượng cao cho chúng dễ tìm.
==== Subversion
(((Subversion)))
(((Importing, from Subversion)))
Nếu bạn đọc phần trước về việc sử dụng `git svn`, bạn có thể dễ dàng sử dụng các hướng dẫn đó để `git svn clone` một kho chứa; sau đó, ngừng sử dụng máy chủ Subversion, đẩy lên một máy chủ Git mới, và bắt đầu sử dụng nó.
Nếu bạn muốn lịch sử, bạn có thể hoàn thành điều đó nhanh như bạn có thể kéo dữ liệu ra khỏi máy chủ Subversion (có thể mất một lúc).
Tuy nhiên, việc nhập không hoàn hảo; và vì nó sẽ mất rất lâu, bạn cũng có thể làm nó đúng cách.
Vấn đề đầu tiên là thông tin tác giả.
Trong Subversion, mỗi người commit có một người dùng trên hệ thống được ghi lại trong thông tin commit.
Các ví dụ trong phần trước hiển thị `schacon` ở một số nơi, chẳng hạn như đầu ra `blame` và `git svn log`.
Nếu bạn muốn ánh xạ điều này sang dữ liệu tác giả Git tốt hơn, bạn cần một ánh xạ từ người dùng Subversion sang tác giả Git.
Tạo một tập tin có tên `users.txt` có ánh xạ này theo định dạng như thế này:
[source]
----
schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>
----
Để lấy danh sách tên tác giả mà SVN sử dụng, bạn có thể chạy lệnh này:
[source,console]
----
$ svn log --xml --quiet | grep author | sort -u | \
perl -pe 's/.*>(.*?)<.*/$1 = /'
----
Lệnh đó tạo ra đầu ra nhật ký ở định dạng XML, sau đó chỉ giữ các dòng có thông tin tác giả, loại bỏ các bản sao, loại bỏ các thẻ XML.
Rõ ràng điều này chỉ hoạt động trên một máy có cài đặt `grep`, `sort`, và `perl`.
Sau đó, chuyển hướng đầu ra đó vào tập tin `users.txt` của bạn để bạn có thể thêm dữ liệu người dùng Git tương đương bên cạnh mỗi mục.
[NOTE]
====
Nếu bạn đang thử điều này trên một máy Windows, đây là điểm mà bạn sẽ gặp rắc rối.
Microsoft đã cung cấp một số lời khuyên và mẫu tốt tại https://learn.microsoft.com/en-us/azure/devops/repos/git/perform-migration-from-svn-to-git[^].
====
Bạn có thể cung cấp tập tin này cho `git svn` để giúp nó ánh xạ dữ liệu tác giả chính xác hơn.
Bạn cũng có thể bảo `git svn` không bao gồm siêu dữ liệu mà Subversion thường nhập, bằng cách truyền `--no-metadata` cho lệnh `clone` hoặc `init`.
Siêu dữ liệu bao gồm một `git-svn-id` bên trong mỗi thông điệp commit mà Git sẽ tạo trong quá trình nhập.
Điều này có thể làm phình to nhật ký Git của bạn và có thể làm cho nó hơi không rõ ràng.
[NOTE]
====
Bạn cần giữ siêu dữ liệu khi bạn muốn phản chiếu các commit được thực hiện trong kho chứa Git trở lại kho chứa SVN gốc.
Nếu bạn không muốn đồng bộ hóa trong nhật ký commit của mình, hãy thoải mái bỏ qua tham số `--no-metadata`.
====
Điều này làm cho lệnh `import` của bạn trông như thế này:
[source,console]
----
$ git svn clone http://my-project.googlecode.com/svn/ \
--authors-file=users.txt --no-metadata --prefix "" -s my_project
$ cd my_project
----
Bây giờ bạn nên có một bản nhập Subversion đẹp hơn trong thư mục `my_project` của bạn.
Thay vì các commit trông như thế này:
[source]
----
commit 37efa680e8473b615de980fa935944215428a35a
Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029>
Date: Sun May 3 00:12:22 2009 +0000
fixed install - go to trunk
git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de-
be05-5f7a86268029
----
chúng trông như thế này:
[source]
----
commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2
Author: Scott Chacon <schacon@geemail.com>
Date: Sun May 3 00:12:22 2009 +0000
fixed install - go to trunk
----
Không chỉ trường Author trông tốt hơn nhiều, mà `git-svn-id` cũng không còn ở đó nữa.
Bạn cũng nên thực hiện một chút dọn dẹp sau khi nhập.
Trước hết, bạn nên dọn dẹp các tham chiếu kỳ lạ mà `git svn` đã thiết lập.
Đầu tiên bạn sẽ di chuyển các thẻ để chúng là các thẻ thực sự thay vì các nhánh từ xa lạ, và sau đó bạn sẽ di chuyển phần còn lại của các nhánh để chúng là cục bộ.
Để di chuyển các thẻ thành các thẻ Git thích hợp, hãy chạy:
[source,console]
----
$ for t in $(git for-each-ref --format='%(refname:short)' refs/remotes/tags); do git tag ${t/tags\//} $t && git branch -D -r $t; done
----
Lệnh này lấy các tham chiếu là các nhánh từ xa bắt đầu bằng `refs/remotes/tags/` và biến chúng thành các thẻ thực sự (nhẹ).
Tiếp theo, di chuyển phần còn lại của các tham chiếu dưới `refs/remotes` thành các nhánh cục bộ:
[source,console]
----
$ for b in $(git for-each-ref --format='%(refname:short)' refs/remotes); do git branch $b refs/remotes/$b && git branch -D -r $b; done
----
Có thể xảy ra rằng bạn sẽ thấy một số nhánh bổ sung được hậu tố bởi `@xxx` (trong đó xxx là một số), trong khi trong Subversion bạn chỉ thấy một nhánh.
Đây thực sự là một tính năng Subversion gọi là "`peg-revisions`", đó là thứ mà Git đơn giản không có đối tác cú pháp.
Do đó, `git svn` chỉ đơn giản thêm số phiên bản SVN vào tên nhánh theo cùng một cách như bạn sẽ viết nó trong SVN để giải quyết peg-revision của nhánh đó.
Nếu bạn không còn quan tâm đến peg-revisions nữa, chỉ cần xóa chúng:
[source,console]
----
$ for p in $(git for-each-ref --format='%(refname:short)' | grep @); do git branch -D $p; done
----
Bây giờ tất cả các nhánh cũ là các nhánh Git thực sự và tất cả các thẻ cũ là các thẻ Git thực sự.
Có một điều cuối cùng để dọn dẹp.
Thật không may, `git svn` tạo một nhánh bổ sung có tên `trunk`, ánh xạ đến nhánh mặc định của Subversion, nhưng tham chiếu `trunk` trỏ đến cùng một nơi như `master`.
Vì `master` là thành ngữ Git hơn, đây là cách xóa nhánh bổ sung:
[source,console]
----
$ git branch -d trunk
----
Điều cuối cùng cần làm là thêm máy chủ Git mới của bạn làm máy từ xa và đẩy lên nó.
Đây là một ví dụ về việc thêm máy chủ của bạn làm máy từ xa:
[source,console]
----
$ git remote add origin git@my-git-server:myrepository.git
----
Vì bạn muốn tất cả các nhánh và thẻ của mình được đẩy lên, bây giờ bạn có thể chạy lệnh này:
[source,console]
----
$ git push origin --all
$ git push origin --tags
----
Tất cả các nhánh và thẻ của bạn nên có trên máy chủ Git mới của bạn trong một bản nhập đẹp, sạch sẽ.
==== Mercurial
(((Mercurial)))(((Importing, from Mercurial)))
Vì Mercurial và Git có các mô hình khá tương tự để biểu diễn các phiên bản, và vì Git linh hoạt hơn một chút, việc chuyển đổi một kho chứa từ Mercurial sang Git khá đơn giản, sử dụng một công cụ gọi là "hg-fast-export", mà bạn sẽ cần một bản sao:
[source,console]
----
$ git clone https://github.com/frej/fast-export.git
----
Bước đầu tiên trong quá trình chuyển đổi là lấy một bản sao đầy đủ của kho chứa Mercurial mà bạn muốn chuyển đổi:
[source,console]
----
$ hg clone <remote repo URL> /tmp/hg-repo
----
Bước tiếp theo là tạo một tập tin ánh xạ tác giả.
Mercurial khoan dung hơn một chút so với Git về những gì nó sẽ đặt trong trường tác giả cho các changeset, vì vậy đây là thời điểm tốt để dọn dẹp.
Tạo điều này là một lệnh một dòng trong shell `bash`:
[source,console]
----
$ cd /tmp/hg-repo
$ hg log | grep user: | sort | uniq | sed 's/user: *//' > ../authors
----
Điều này sẽ mất vài giây, tùy thuộc vào lịch sử dự án của bạn dài bao lâu, và sau đó tập tin `/tmp/authors` sẽ trông giống như thế này:
[source]
----
bob
bob@localhost
bob <bob@company.com>
bob jones <bob <AT> company <DOT> com>
Bob Jones <bob@company.com>
Joe Smith <joe@company.com>
----
Trong ví dụ này, cùng một người (Bob) đã tạo các changeset dưới bốn tên khác nhau, một trong số đó thực sự trông đúng, và một trong số đó sẽ hoàn toàn không hợp lệ cho một commit Git.
Hg-fast-export cho phép chúng ta sửa điều này bằng cách biến mỗi dòng thành một quy tắc: `"<input>"="<output>"`, ánh xạ một `<input>` sang một `<output>`.
Bên trong các chuỗi `<input>` và `<output>`, tất cả các chuỗi thoát được hiểu bởi mã hóa `string_escape` của Python đều được hỗ trợ.
Nếu tập tin ánh xạ tác giả không chứa `<input>` khớp, tác giả đó sẽ được gửi đến Git mà không được sửa đổi.
Nếu tất cả tên người dùng trông ổn, chúng ta sẽ không cần tập tin này chút nào.
Trong ví dụ này, chúng ta muốn tập tin của mình trông như thế này:
[source]
----
"bob"="Bob Jones <bob@company.com>"
"bob@localhost"="Bob Jones <bob@company.com>"
"bob <bob@company.com>"="Bob Jones <bob@company.com>"
"bob jones <bob <AT> company <DOT> com>"="Bob Jones <bob@company.com>"
----
Cùng loại tập tin ánh xạ có thể được sử dụng để đổi tên các nhánh và thẻ khi tên Mercurial không được Git cho phép.
Bước tiếp theo là tạo kho chứa Git mới của chúng ta, và chạy tập tin kịch bản xuất:
[source,console]
----
$ git init /tmp/converted
$ cd /tmp/converted
$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors
----
Cờ `-r` bảo hg-fast-export tìm kho chứa Mercurial mà chúng ta muốn chuyển đổi ở đâu, và cờ `-A` bảo nó tìm tập tin ánh xạ tác giả ở đâu (các tập tin ánh xạ nhánh và thẻ được chỉ định bởi các cờ `-B` và `-T` tương ứng).
Tập tin kịch bản phân tích cú pháp các changeset Mercurial và chuyển đổi chúng thành một tập tin kịch bản cho tính năng "fast-import" của Git (mà chúng ta sẽ thảo luận chi tiết hơn một chút sau).
Điều này mất một chút (mặc dù nó _nhanh hơn nhiều_ so với nó sẽ là qua mạng), và đầu ra khá dài dòng:
[source,console]
----
$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors
Loaded 4 authors
master: Exporting full revision 1/22208 with 13/0/0 added/changed/removed files
master: Exporting simple delta revision 2/22208 with 1/1/0 added/changed/removed files
master: Exporting simple delta revision 3/22208 with 0/1/0 added/changed/removed files
[…]
master: Exporting simple delta revision 22206/22208 with 0/4/0 added/changed/removed files
master: Exporting simple delta revision 22207/22208 with 0/2/0 added/changed/removed files
master: Exporting thorough delta revision 22208/22208 with 3/213/0 added/changed/removed files
Exporting tag [0.4c] at [hg r9] [git :10]
Exporting tag [0.4d] at [hg r16] [git :17]
[…]
Exporting tag [3.1-rc] at [hg r21926] [git :21927]
Exporting tag [3.1] at [hg r21973] [git :21974]
Issued 22315 commands
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects: 120000
Total objects: 115032 ( 208171 duplicates )
blobs : 40504 ( 205320 duplicates 26117 deltas of 39602 attempts)
trees : 52320 ( 2851 duplicates 47467 deltas of 47599 attempts)
commits: 22208 ( 0 duplicates 0 deltas of 0 attempts)
tags : 0 ( 0 duplicates 0 deltas of 0 attempts)
Total branches: 109 ( 2 loads )
marks: 1048576 ( 22208 unique )
atoms: 1952
Memory total: 7860 KiB
pools: 2235 KiB
objects: 5625 KiB
---------------------------------------------------------------------
pack_report: getpagesize() = 4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit = 8589934592
pack_report: pack_used_ctr = 90430
pack_report: pack_mmap_calls = 46771
pack_report: pack_open_windows = 1 / 1
pack_report: pack_mapped = 340852700 / 340852700
---------------------------------------------------------------------
$ git shortlog -sn
369 Bob Jones
365 Joe Smith
----
Đó là khá nhiều tất cả những gì có.
Tất cả các thẻ Mercurial đã được chuyển đổi thành các thẻ Git, và các nhánh và dấu trang Mercurial đã được chuyển đổi thành các nhánh Git.
Bây giờ bạn đã sẵn sàng đẩy kho chứa lên nhà mới phía máy chủ của nó:
[source,console]
----
$ git remote add origin git@my-git-server:myrepository.git
$ git push origin --all
----
[[_perforce_import]]
==== Perforce
(((Perforce)))(((Importing, from Perforce)))
Hệ thống tiếp theo bạn sẽ xem xét việc nhập từ đó là Perforce.
Như chúng ta đã thảo luận ở trên, có hai cách để cho Git và Perforce nói chuyện với nhau: git-p4 và Perforce Git Fusion.
===== Perforce Git Fusion
Git Fusion làm cho quá trình này khá không đau đớn.
Chỉ cần cấu hình các thiết lập dự án, ánh xạ người dùng, và các nhánh của bạn bằng cách sử dụng một tập tin cấu hình (như đã thảo luận trong <<_p4_git_fusion>>), và sao chép kho chứa.
Git Fusion để lại cho bạn những gì trông giống như một kho chứa Git bản địa, sau đó sẵn sàng để đẩy lên một máy chủ Git bản địa nếu bạn muốn.
Bạn thậm chí có thể sử dụng Perforce làm máy chủ Git của bạn nếu bạn thích.
[[_git_p4]]
===== Git-p4
Git-p4 cũng có thể hoạt động như một công cụ nhập.
Như một ví dụ, chúng ta sẽ nhập dự án Jam từ Perforce Public Depot.
Để thiết lập máy khách của bạn, bạn phải xuất biến môi trường P4PORT để trỏ đến depot Perforce:
[source,console]
----
$ export P4PORT=public.perforce.com:1666
----
[NOTE]
====
Để theo dõi, bạn sẽ cần một depot Perforce để kết nối.
Chúng tôi sẽ sử dụng depot công khai tại public.perforce.com cho các ví dụ của chúng tôi, nhưng bạn có thể sử dụng bất kỳ depot nào bạn có quyền truy cập.
====
(((git commands, p4)))
Chạy lệnh `git p4 clone` để nhập dự án Jam từ máy chủ Perforce, cung cấp đường dẫn depot và dự án và đường dẫn mà bạn muốn nhập dự án vào:
[source,console]
----
$ git-p4 clone //guest/perforce_software/jam@all p4import
Importing from //guest/perforce_software/jam@all into p4import
Initialized empty Git repository in /private/tmp/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 9957 (100%)
----
Dự án cụ thể này chỉ có một nhánh, nhưng nếu bạn có các nhánh được cấu hình với các view nhánh (hoặc chỉ là một tập hợp các thư mục), bạn có thể sử dụng cờ `--detect-branches` cho `git p4 clone` để nhập tất cả các nhánh của dự án.
Xem <<_git_p4_branches>> để biết thêm chi tiết về điều này.
Tại thời điểm này bạn gần như hoàn thành.
Nếu bạn đi đến thư mục `p4import` và chạy `git log`, bạn có thể thấy công việc đã nhập của mình:
[source,console]
----
$ git log -2
commit e5da1c909e5db3036475419f6379f2c73710c4e6
Author: giles <giles@giles@perforce.com>
Date: Wed Feb 8 03:13:27 2012 -0800
Correction to line 355; change </UL> to </OL>.
[git-p4: depot-paths = "//public/jam/src/": change = 8068]
commit aa21359a0a135dda85c50a7f7cf249e4f7b8fd98
Author: kwirth <kwirth@perforce.com>
Date: Tue Jul 7 01:35:51 2009 -0800
Fix spelling error on Jam doc page (cummulative -> cumulative).
[git-p4: depot-paths = "//public/jam/src/": change = 7304]
----
Bạn có thể thấy rằng `git-p4` đã để lại một định danh trong mỗi thông điệp commit.
Không sao khi giữ định danh đó ở đó, trong trường hợp bạn cần tham chiếu số thay đổi Perforce sau này.
Tuy nhiên, nếu bạn muốn xóa định danh, bây giờ là lúc để làm như vậy – trước khi bạn bắt đầu thực hiện công việc trên kho chứa mới.
(((git commands, filter-branch)))
Bạn có thể sử dụng `git filter-branch` để xóa các chuỗi định danh hàng loạt:
[source,console]
----
$ git filter-branch --msg-filter 'sed -e "/^\[git-p4:/d"'
Rewrite e5da1c909e5db3036475419f6379f2c73710c4e6 (125/125)
Ref 'refs/heads/master' was rewritten
----
Nếu bạn chạy `git log`, bạn có thể thấy rằng tất cả các tổng kiểm tra SHA-1 cho các commit đã thay đổi, nhưng các chuỗi `git-p4` không còn trong các thông điệp commit nữa:
[source,console]
----
$ git log -2
commit b17341801ed838d97f7800a54a6f9b95750839b7
Author: giles <giles@giles@perforce.com>
Date: Wed Feb 8 03:13:27 2012 -0800
Correction to line 355; change </UL> to </OL>.
commit 3e68c2e26cd89cb983eb52c024ecdfba1d6b3fff
Author: kwirth <kwirth@perforce.com>
Date: Tue Jul 7 01:35:51 2009 -0800
Fix spelling error on Jam doc page (cummulative -> cumulative).
----
Bản nhập của bạn đã sẵn sàng để đẩy lên máy chủ Git mới của bạn.
[[_custom_importer]]
==== Một Trình Nhập Tùy chỉnh
(((git commands, fast-import)))
(((Importing, from others)))
Nếu hệ thống của bạn không phải là một trong những hệ thống trên, bạn nên tìm kiếm một trình nhập trực tuyến – các trình nhập chất lượng có sẵn cho nhiều hệ thống khác, bao gồm CVS, Clear Case, Visual Source Safe, thậm chí cả một thư mục lưu trữ.
Nếu không có công cụ nào trong số này hoạt động cho bạn, bạn có một công cụ khó hiểu hơn, hoặc bạn cần một quy trình nhập tùy chỉnh hơn, bạn nên sử dụng `git fast-import`.
Lệnh này đọc các hướng dẫn đơn giản từ stdin để ghi dữ liệu Git cụ thể.
Cách này dễ dàng hơn nhiều để tạo các đối tượng Git so với chạy các lệnh Git thô hoặc cố gắng ghi các đối tượng thô (xem <<ch10-git-internals#ch10-git-internals>> để biết thêm thông tin).
Bằng cách này, bạn có thể viết một tập tin kịch bản nhập đọc thông tin cần thiết ra khỏi hệ thống bạn đang nhập và in các hướng dẫn đơn giản ra stdout.
Sau đó, bạn có thể chạy chương trình này và chuyển đầu ra của nó qua `git fast-import`.
Để minh họa nhanh, bạn sẽ viết một trình nhập đơn giản.
Giả sử bạn làm việc trong `current`, bạn sao lưu dự án của mình bằng cách thỉnh thoảng sao chép thư mục vào một thư mục sao lưu `back_YYYY_MM_DD` có dấu thời gian, và bạn muốn nhập điều này vào Git.
Cấu trúc thư mục của bạn trông như thế này:
[source,console]
----
$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current
----
Để nhập một thư mục Git, bạn cần xem xét cách Git lưu trữ dữ liệu của nó.
Như bạn có thể nhớ, Git về cơ bản là một danh sách liên kết của các đối tượng commit trỏ đến một ảnh chụp nhanh của nội dung.
Tất cả những gì bạn phải làm là bảo `fast-import` các ảnh chụp nhanh nội dung là gì, dữ liệu commit nào trỏ đến chúng, và thứ tự chúng đi vào.
Chiến lược của bạn sẽ là đi qua các ảnh chụp nhanh từng cái một và tạo các commit với nội dung của mỗi thư mục, liên kết mỗi commit trở lại cái trước đó.
Như chúng ta đã làm trong <<ch08-customizing-git#_an_example_git_enforced_policy>>, chúng ta sẽ viết điều này bằng Ruby, bởi vì đó là những gì chúng ta thường làm việc và nó có xu hướng dễ đọc.
Bạn có thể viết ví dụ này khá dễ dàng trong bất cứ thứ gì bạn quen thuộc – nó chỉ cần in thông tin thích hợp ra `stdout`.
Và, nếu bạn đang chạy trên Windows, điều này có nghĩa là bạn sẽ cần chú ý đặc biệt để không đưa các ký tự xuống dòng kiểu carriage return vào cuối dòng của bạn – `git fast-import` rất cụ thể về việc chỉ muốn line feeds (LF) chứ không phải carriage return line feeds (CRLF) mà Windows sử dụng.
Để bắt đầu, bạn sẽ chuyển vào thư mục đích và xác định mọi thư mục con, mỗi thư mục là một ảnh chụp nhanh mà bạn muốn nhập dưới dạng một commit.
Bạn sẽ chuyển vào mỗi thư mục con và in các lệnh cần thiết để xuất nó.
Vòng lặp chính cơ bản của bạn trông như thế này:
[source,ruby]
----
last_mark = nil
# loop through the directories
Dir.chdir(ARGV[0]) do
Dir.glob("*").each do |dir|
next if File.file?(dir)
# move into the target directory
Dir.chdir(dir) do
last_mark = print_export(dir, last_mark)
end
end
end
----
Bạn chạy `print_export` bên trong mỗi thư mục, lấy bảng kê khai và dấu của ảnh chụp nhanh trước đó và trả về bảng kê khai và dấu của cái này; bằng cách đó, bạn có thể liên kết chúng đúng cách.
"`Mark`" là thuật ngữ `fast-import` cho một định danh bạn cung cấp cho một commit; khi bạn tạo các commit, bạn cung cấp cho mỗi cái một dấu mà bạn có thể sử dụng để liên kết đến nó từ các commit khác.
Vì vậy, điều đầu tiên cần làm trong phương thức `print_export` của bạn là tạo một dấu từ tên thư mục:
[source,ruby]
----
mark = convert_dir_to_mark(dir)
----
Bạn sẽ làm điều này bằng cách tạo một mảng các thư mục và sử dụng giá trị chỉ mục làm dấu, bởi vì một dấu phải là một số nguyên.
Phương thức của bạn trông như thế này:
[source,ruby]
----
$marks = []
def convert_dir_to_mark(dir)
if !$marks.include?(dir)
$marks << dir
end
($marks.index(dir) + 1).to_s
end
----
Bây giờ bạn có một biểu diễn số nguyên của commit của mình, bạn cần một ngày cho siêu dữ liệu commit.
Vì ngày được biểu thị trong tên của thư mục, bạn sẽ phân tích cú pháp nó ra.
Dòng tiếp theo trong tập tin `print_export` của bạn là:
[source,ruby]
----
date = convert_dir_to_date(dir)
----
trong đó `convert_dir_to_date` được định nghĩa là:
[source,ruby]
----
def convert_dir_to_date(dir)
if dir == 'current'
return Time.now().to_i
else
dir = dir.gsub('back_', '')
(year, month, day) = dir.split('_')
return Time.local(year, month, day).to_i
end
end
----
Điều đó trả về một giá trị số nguyên cho ngày của mỗi thư mục.
Phần thông tin meta cuối cùng bạn cần cho mỗi commit là dữ liệu người commit, mà bạn mã hóa cứng trong một biến toàn cục:
[source,ruby]
----
$author = 'John Doe <john@example.com>'
----
Bây giờ bạn đã sẵn sàng để bắt đầu in ra dữ liệu commit cho trình nhập của mình.
Thông tin ban đầu nói rằng bạn đang định nghĩa một đối tượng commit và nhánh nào nó đang ở, theo sau là dấu bạn đã tạo, thông tin người commit và thông điệp commit, và sau đó là commit trước đó, nếu có.
Mã trông như thế này:
[source,ruby]
----
# print the import information
puts 'commit refs/heads/master'
puts 'mark :' + mark
puts "committer #{$author} #{date} -0700"
export_data('imported from ' + dir)
puts 'from :' + last_mark if last_mark
----
Bạn mã hóa cứng múi giờ (-0700) vì làm như vậy là dễ dàng.
Nếu bạn đang nhập từ một hệ thống khác, bạn phải chỉ định múi giờ như một độ lệch.
Thông điệp commit phải được biểu thị theo một định dạng đặc biệt:
[source]
----
data (size)\n(contents)
----
Định dạng bao gồm từ data, kích thước của dữ liệu cần đọc, một dòng mới, và cuối cùng là dữ liệu.
Vì bạn cần sử dụng cùng một định dạng để chỉ định nội dung tập tin sau này, bạn tạo một phương thức trợ giúp, `export_data`:
[source,ruby]
----
def export_data(string)
print "data #{string.size}\n#{string}"
end
----
Tất cả những gì còn lại là chỉ định nội dung tập tin cho mỗi ảnh chụp nhanh.
Điều này dễ dàng, vì bạn có mỗi cái trong một thư mục – bạn có thể in ra lệnh `deleteall` theo sau là nội dung của mỗi tập tin trong thư mục.
Git sau đó sẽ ghi lại mỗi ảnh chụp nhanh một cách thích hợp:
[source,ruby]
----
puts 'deleteall'
Dir.glob("**/*").each do |file|
next if !File.file?(file)
inline_data(file)
end
----
Lưu ý: Vì nhiều hệ thống nghĩ về các bản sửa đổi của họ như các thay đổi từ một commit sang một commit khác, fast-import cũng có thể nhận các lệnh với mỗi commit để chỉ định tập tin nào đã được thêm, xóa, hoặc sửa đổi và nội dung mới là gì.
Bạn có thể tính toán sự khác biệt giữa các ảnh chụp nhanh và chỉ cung cấp dữ liệu này, nhưng làm như vậy phức tạp hơn – bạn cũng có thể cung cấp cho Git tất cả dữ liệu và để nó tìm ra.
Nếu điều này phù hợp hơn với dữ liệu của bạn, hãy kiểm tra trang man `fast-import` để biết chi tiết về cách cung cấp dữ liệu của bạn theo cách này.
Định dạng để liệt kê nội dung tập tin mới hoặc chỉ định một tập tin đã sửa đổi với nội dung mới như sau:
[source]
----
M 644 inline path/to/file
data (size)
(file contents)
----
Ở đây, 644 là chế độ (nếu bạn có các tập tin thực thi, bạn cần phát hiện và chỉ định 755 thay thế), và inline nói rằng bạn sẽ liệt kê nội dung ngay sau dòng này.
Phương thức `inline_data` của bạn trông như thế này:
[source,ruby]
----
def inline_data(file, code = 'M', mode = '644')
content = File.read(file)
puts "#{code} #{mode} inline #{file}"
export_data(content)
end
----
Bạn tái sử dụng phương thức `export_data` bạn đã định nghĩa trước đó, vì nó giống như cách bạn chỉ định dữ liệu thông điệp commit của mình.
Điều cuối cùng bạn cần làm là trả về dấu hiện tại để nó có thể được truyền cho lần lặp tiếp theo:
[source,ruby]
----
return mark
----
[NOTE]
====
Nếu bạn đang chạy trên Windows, bạn sẽ cần đảm bảo rằng bạn thêm một bước bổ sung.
Như đã đề cập trước đó, Windows sử dụng CRLF cho các ký tự dòng mới trong khi `git fast-import` chỉ mong đợi LF.
Để giải quyết vấn đề này và làm cho `git fast-import` hài lòng, bạn cần bảo ruby sử dụng LF thay vì CRLF:
[source,ruby]
----
$stdout.binmode
----
====
Vậy là xong.
Đây là tập tin kịch bản trong toàn bộ của nó:
[source,ruby]
----
#!/usr/bin/env ruby
$stdout.binmode
$author = "John Doe <john@example.com>"
$marks = []
def convert_dir_to_mark(dir)
if !$marks.include?(dir)
$marks << dir
end
($marks.index(dir)+1).to_s
end
def convert_dir_to_date(dir)
if dir == 'current'
return Time.now().to_i
else
dir = dir.gsub('back_', '')
(year, month, day) = dir.split('_')
return Time.local(year, month, day).to_i
end
end
def export_data(string)
print "data #{string.size}\n#{string}"
end
def inline_data(file, code='M', mode='644')
content = File.read(file)
puts "#{code} #{mode} inline #{file}"
export_data(content)
end
def print_export(dir, last_mark)
date = convert_dir_to_date(dir)
mark = convert_dir_to_mark(dir)
puts 'commit refs/heads/master'
puts "mark :#{mark}"
puts "committer #{$author} #{date} -0700"
export_data("imported from #{dir}")
puts "from :#{last_mark}" if last_mark
puts 'deleteall'
Dir.glob("**/*").each do |file|
next if !File.file?(file)
inline_data(file)
end
mark
end
# Loop through the directories
last_mark = nil
Dir.chdir(ARGV[0]) do
Dir.glob("*").each do |dir|
next if File.file?(dir)
# move into the target directory
Dir.chdir(dir) do
last_mark = print_export(dir, last_mark)
end
end
end
----
Nếu bạn chạy tập tin kịch bản này, bạn sẽ nhận được nội dung trông giống như thế này:
[source,console]
----
$ ruby import.rb /opt/import_from
commit refs/heads/master
mark :1
committer John Doe <john@example.com> 1388649600 -0700
data 29
imported from back_2014_01_02deleteall
M 644 inline README.md
data 28
# Hello
This is my readme.
commit refs/heads/master
mark :2
committer John Doe <john@example.com> 1388822400 -0700
data 29
imported from back_2014_01_04from :1
deleteall
M 644 inline main.rb
data 34
#!/bin/env ruby
puts "Hey there"
M 644 inline README.md
(...)
----
Để chạy trình nhập, hãy chuyển đầu ra này qua `git fast-import` trong khi ở trong thư mục Git bạn muốn nhập vào.
Bạn có thể tạo một thư mục mới và sau đó chạy `git init` trong đó cho một điểm khởi đầu, và sau đó chạy tập tin kịch bản của bạn:
[source,console]
----
$ git init
Initialized empty Git repository in /opt/import_to/.git/
$ ruby import.rb /opt/import_from | git fast-import
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects: 5000
Total objects: 13 ( 6 duplicates )
blobs : 5 ( 4 duplicates 3 deltas of 5 attempts)
trees : 4 ( 1 duplicates 0 deltas of 4 attempts)
commits: 4 ( 1 duplicates 0 deltas of 0 attempts)
tags : 0 ( 0 duplicates 0 deltas of 0 attempts)
Total branches: 1 ( 1 loads )
marks: 1024 ( 5 unique )
atoms: 2
Memory total: 2344 KiB
pools: 2110 KiB
objects: 234 KiB
---------------------------------------------------------------------
pack_report: getpagesize() = 4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit = 8589934592
pack_report: pack_used_ctr = 10
pack_report: pack_mmap_calls = 5
pack_report: pack_open_windows = 2 / 2
pack_report: pack_mapped = 1457 / 1457
---------------------------------------------------------------------
----
Như bạn có thể thấy, khi nó hoàn thành thành công, nó cung cấp cho bạn một loạt thống kê về những gì nó đã hoàn thành.
Trong trường hợp này, bạn đã nhập tổng cộng 13 đối tượng cho 4 commit vào 1 nhánh.
Bây giờ, bạn có thể chạy `git log` để xem lịch sử mới của mình:
[source,console]
----
$ git log -2
commit 3caa046d4aac682a55867132ccdfbe0d3fdee498
Author: John Doe <john@example.com>
Date: Tue Jul 29 19:39:04 2014 -0700
imported from current
commit 4afc2b945d0d3c8cd00556fbe2e8224569dc9def
Author: John Doe <john@example.com>
Date: Mon Feb 3 01:00:00 2014 -0700
imported from back_2014_02_03
----
Đó rồi – một kho chứa Git đẹp, sạch sẽ.
Điều quan trọng cần lưu ý là không có gì được check out – bạn không có bất kỳ tập tin nào trong thư mục làm việc của mình lúc đầu.
Để lấy chúng, bạn phải reset nhánh của mình đến nơi `master` hiện tại:
[source,console]
----
$ ls
$ git reset --hard master
HEAD is now at 3caa046 imported from current
$ ls
README.md main.rb
----
Bạn có thể làm nhiều hơn nữa với công cụ `fast-import` – xử lý các chế độ khác nhau, dữ liệu nhị phân, nhiều nhánh và trộn, thẻ, chỉ báo tiến trình, và nhiều hơn nữa.
Một số ví dụ về các tình huống phức tạp hơn có sẵn trong thư mục `contrib/fast-import` của mã nguồn Git.
=== Tóm tắt
Bạn sẽ cảm thấy thoải mái khi dùng Git như một client cho các hệ thống quản lý phiên bản khác, hoặc khi nhập hầu như bất kỳ kho hiện có nào vào Git mà không mất dữ liệu.
Trong chương tiếp theo, chúng tôi sẽ đề cập đến phần nội bộ (internals) của Git để bạn có thể tùy chỉnh từng byte nếu cần.
[[ch10-git-internals]]
== Git Internals
You may have skipped to this chapter from a much earlier chapter, or you may have gotten here after sequentially reading the entire book up to this point -- in either case, this is where we'll go over the inner workings and implementation of Git.
We found that understanding this information was fundamentally important to appreciating how useful and powerful Git is, but others have argued to us that it can be confusing and unnecessarily complex for beginners.
Thus, we've made this discussion the last chapter in the book so you could read it early or later in your learning process.
We leave it up to you to decide.
Now that you're here, let's get started.
First, if it isn't yet clear, Git is fundamentally a content-addressable filesystem with a VCS user interface written on top of it.
You'll learn more about what this means in a bit.
In the early days of Git (mostly pre 1.5), the user interface was much more complex because it emphasized this filesystem rather than a polished VCS.
Trong vài năm gần đây, giao diện đã được tinh chỉnh cho sạch và dễ dùng, tuy nhiên vẫn còn định kiến về giao diện Git ban đầu là phức tạp và khó học.
The content-addressable filesystem layer is amazingly cool, so we'll cover that first in this chapter; then, you'll learn about the transport mechanisms and the repository maintenance tasks that you may eventually have to deal with.
[[_plumbing_porcelain]]
=== Plumbing và Porcelain
Cuốn sách này chủ yếu đề cập đến cách sử dụng Git với khoảng 30 lệnh con như `checkout`, `branch`, `remote`, v.v.
Nhưng vì Git ban đầu là một bộ công cụ cho một hệ thống kiểm soát phiên bản thay vì một VCS thân thiện với người dùng hoàn chỉnh, nó có một số lệnh con thực hiện công việc cấp thấp và được thiết kế để được xâu chuỗi lại với nhau theo kiểu UNIX hoặc được gọi từ các tập tin kịch bản.
Những lệnh này thường được gọi là các lệnh "`plumbing`" (ống nước) của Git, trong khi các lệnh thân thiện với người dùng hơn được gọi là các lệnh "`porcelain`" (sứ).
Như bạn sẽ nhận thấy đến bây giờ, chín chương đầu tiên của cuốn sách này gần như chỉ đề cập đến các lệnh porcelain.
Nhưng trong chương này, bạn sẽ chủ yếu làm việc với các lệnh plumbing cấp thấp hơn, bởi vì chúng cho bạn quyền truy cập vào hoạt động bên trong của Git, và giúp chứng minh cách thức và lý do tại sao Git làm những gì nó làm.
Nhiều lệnh trong số này không có nghĩa là được sử dụng thủ công trên dòng lệnh, mà là được sử dụng như các khối xây dựng cho các công cụ mới và các tập tin kịch bản tùy chỉnh.
Khi bạn chạy `git init` trong một thư mục mới hoặc hiện có, Git tạo thư mục `.git`, đó là nơi gần như mọi thứ mà Git lưu trữ và thao tác được đặt.
Nếu bạn muốn sao lưu hoặc sao chép kho chứa của mình, việc sao chép thư mục duy nhất này sang nơi khác sẽ cung cấp cho bạn gần như mọi thứ bạn cần.
Toàn bộ chương này về cơ bản đề cập đến những gì bạn có thể thấy trong thư mục này.
Đây là những gì một thư mục `.git` mới được khởi tạo thường trông như thế nào:
[source,console]
----
$ ls -F1
config
description
HEAD
hooks/
info/
objects/
refs/
----
Tùy thuộc vào phiên bản Git của bạn, bạn có thể thấy một số nội dung bổ sung ở đó, nhưng đây là một kho chứa `git init` mới – đó là những gì bạn thấy theo mặc định.
Tập tin `description` chỉ được sử dụng bởi chương trình GitWeb, vì vậy đừng lo lắng về nó.
Tập tin `config` chứa các tùy chọn cấu hình cụ thể cho dự án của bạn, và thư mục `info` giữ một tập tin loại trừ toàn cục (((excludes))) cho các mẫu bị bỏ qua mà bạn không muốn theo dõi trong một tập tin `.gitignore`.
Thư mục `hooks` chứa các tập tin kịch bản móc phía máy khách hoặc phía máy chủ của bạn, được thảo luận chi tiết trong <<ch08-customizing-git#_git_hooks>>.
Điều này để lại bốn mục quan trọng: các tập tin `HEAD` và (chưa được tạo) `index`, và các thư mục `objects` và `refs`.
Đây là các phần cốt lõi của Git.
Thư mục `objects` lưu trữ tất cả nội dung cho cơ sở dữ liệu của bạn, thư mục `refs` lưu trữ các con trỏ vào các đối tượng commit trong dữ liệu đó (branches, tags, remotes và nhiều hơn nữa), tập tin `HEAD` trỏ đến nhánh bạn hiện đang check out, và tập tin `index` là nơi Git lưu trữ thông tin khu vực tổ chức (staging area) của bạn.
Bây giờ bạn sẽ xem xét từng phần này một cách chi tiết để xem Git hoạt động như thế nào.
[[_objects]]
=== Đối tượng Git
Git là một hệ thống tập tin có thể định địa chỉ nội dung (content-addressable filesystem).
Tuyệt vời.
Điều đó có nghĩa là gì?
Nó có nghĩa là ở cốt lõi của Git là một kho lưu trữ dữ liệu khóa-giá trị đơn giản.
Điều này có nghĩa là bạn có thể chèn bất kỳ loại nội dung nào vào một kho chứa Git, mà Git sẽ trả lại cho bạn một khóa duy nhất mà bạn có thể sử dụng sau này để truy xuất nội dung đó.
Để minh họa, hãy xem lệnh plumbing `git hash-object`, lệnh này nhận một số dữ liệu, lưu trữ nó trong thư mục `.git/objects` của bạn (_cơ sở dữ liệu đối tượng_), và trả lại cho bạn khóa duy nhất hiện đang tham chiếu đến đối tượng dữ liệu đó.
Đầu tiên, bạn khởi tạo một kho chứa Git mới và xác minh rằng (có thể dự đoán được) không có gì trong thư mục `objects`:
[source,console]
----
$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f
----
Git đã khởi tạo thư mục `objects` và tạo các thư mục con `pack` và `info` trong đó, nhưng không có tập tin thông thường nào.
Bây giờ, hãy sử dụng `git hash-object` để tạo một đối tượng dữ liệu mới và lưu trữ thủ công nó trong cơ sở dữ liệu Git mới của bạn:
[source,console]
----
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
----
Ở dạng đơn giản nhất, `git hash-object` sẽ lấy nội dung bạn đưa cho nó và chỉ trả về khóa duy nhất _sẽ_ được sử dụng để lưu trữ nó trong cơ sở dữ liệu Git của bạn.
Tùy chọn `-w` sau đó bảo lệnh không chỉ đơn giản trả về khóa, mà còn ghi đối tượng đó vào cơ sở dữ liệu.
Cuối cùng, tùy chọn `--stdin` bảo `git hash-object` lấy nội dung cần xử lý từ stdin; nếu không, lệnh sẽ mong đợi một đối số tên tập tin ở cuối lệnh chứa nội dung sẽ được sử dụng.
Đầu ra từ lệnh trên là một hàm băm tổng kiểm tra 40 ký tự.
Đây là hàm băm SHA-1 -- một tổng kiểm tra của nội dung bạn đang lưu trữ cộng với một tiêu đề, mà bạn sẽ tìm hiểu trong một chút.
Bây giờ bạn có thể thấy Git đã lưu trữ dữ liệu của bạn như thế nào:
[source,console]
----
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
----
Nếu bạn lại kiểm tra thư mục `objects` của mình, bạn có thể thấy rằng bây giờ nó chứa một tập tin cho nội dung mới đó.
Đây là cách Git lưu trữ nội dung ban đầu -- như một tập tin duy nhất cho mỗi phần nội dung, được đặt tên bằng tổng kiểm tra SHA-1 của nội dung và tiêu đề của nó.
Thư mục con được đặt tên bằng 2 ký tự đầu tiên của SHA-1, và tên tập tin là 38 ký tự còn lại.
Khi bạn đã có nội dung trong cơ sở dữ liệu đối tượng của mình, bạn có thể kiểm tra nội dung đó bằng lệnh `git cat-file`.
Lệnh này giống như một con dao đa năng Thụy Sĩ để kiểm tra các đối tượng Git.
Truyền `-p` cho `cat-file` hướng dẫn lệnh trước tiên tìm ra loại nội dung, sau đó hiển thị nó một cách thích hợp:
[source,console]
----
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
----
Bây giờ, bạn có thể thêm nội dung vào Git và kéo nó ra lại.
Bạn cũng có thể làm điều này với nội dung trong các tập tin.
Ví dụ, bạn có thể thực hiện một số kiểm soát phiên bản đơn giản trên một tập tin.
Đầu tiên, tạo một tập tin mới và lưu nội dung của nó trong cơ sở dữ liệu của bạn:
[source,console]
----
$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
----
Sau đó, ghi một số nội dung mới vào tập tin, và lưu nó lại:
[source,console]
----
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
----
Cơ sở dữ liệu đối tượng của bạn bây giờ chứa cả hai phiên bản của tập tin mới này (cũng như nội dung đầu tiên bạn đã lưu trữ ở đó):
[source,console]
----
$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
----
Tại thời điểm này, bạn có thể xóa bản sao cục bộ của tập tin `test.txt` đó, sau đó sử dụng Git để truy xuất, từ cơ sở dữ liệu đối tượng, phiên bản đầu tiên bạn đã lưu:
[source,console]
----
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1
----
hoặc phiên bản thứ hai:
[source,console]
----
$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2
----
Nhưng việc nhớ khóa SHA-1 cho mỗi phiên bản của tập tin của bạn không thực tế; cộng thêm, bạn không lưu trữ tên tập tin trong hệ thống của mình -- chỉ là nội dung.
Loại đối tượng này được gọi là _blob_.
Bạn có thể yêu cầu Git cho bạn biết loại đối tượng của bất kỳ đối tượng nào trong Git, với khóa SHA-1 của nó, bằng `git cat-file -t`:
[source,console]
----
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob
----
[[_tree_objects]]
==== Đối tượng Tree
Loại đối tượng Git tiếp theo chúng ta sẽ kiểm tra là _tree_, giải quyết vấn đề lưu trữ tên tập tin và cũng cho phép bạn lưu trữ một nhóm tập tin cùng nhau.
Git lưu trữ nội dung theo cách tương tự như một hệ thống tập tin UNIX, nhưng đơn giản hóa một chút.
Tất cả nội dung được lưu trữ dưới dạng các đối tượng tree và blob, với các tree tương ứng với các mục thư mục UNIX và các blob tương ứng ít nhiều với các inode hoặc nội dung tập tin.
Một đối tượng tree duy nhất chứa một hoặc nhiều mục, mỗi mục là hàm băm SHA-1 của một blob hoặc subtree với chế độ, loại và tên tập tin liên quan của nó.
Ví dụ, giả sử bạn có một dự án trong đó tree gần đây nhất trông giống như:
[source,console]
----
$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib
----
Cú pháp `master^{tree}` chỉ định đối tượng tree được trỏ đến bởi commit cuối cùng trên nhánh `master` của bạn.
Lưu ý rằng thư mục con `lib` không phải là một blob mà là một con trỏ đến một tree khác:
[source,console]
----
$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb
----
[NOTE]
====
Tùy thuộc vào shell bạn sử dụng, bạn có thể gặp lỗi khi sử dụng cú pháp `master^{tree}`.
Trong CMD trên Windows, ký tự `^` được sử dụng để thoát, vì vậy bạn phải nhân đôi nó để tránh điều này: `git cat-file -p master^^{tree}`.
Khi sử dụng PowerShell, các tham số sử dụng ký tự `{}` phải được đặt trong dấu ngoặc kép để tránh tham số bị phân tích cú pháp không chính xác: `git cat-file -p 'master^{tree}'`.
Nếu bạn đang sử dụng ZSH, ký tự `^` được sử dụng cho globbing, vì vậy bạn phải đặt toàn bộ biểu thức trong dấu ngoặc kép: `git cat-file -p "master^{tree}"`.
====
Về mặt khái niệm, dữ liệu mà Git đang lưu trữ trông giống như thế này:
.Phiên bản đơn giản của mô hình dữ liệu Git
image::images/data-model-1.png[Phiên bản đơn giản của mô hình dữ liệu Git]
Bạn có thể khá dễ dàng tạo tree của riêng mình.
Git thường tạo một tree bằng cách lấy trạng thái của khu vực tổ chức hoặc index của bạn và ghi một loạt các đối tượng tree từ đó.
Vì vậy, để tạo một đối tượng tree, trước tiên bạn phải thiết lập một index bằng cách tổ chức một số tập tin.
Để tạo một index với một mục duy nhất -- phiên bản đầu tiên của tập tin `test.txt` của bạn -- bạn có thể sử dụng lệnh plumbing `git update-index`.
Bạn sử dụng lệnh này để thêm một cách giả tạo phiên bản trước đó của tập tin `test.txt` vào một khu vực tổ chức mới.
Bạn phải truyền cho nó tùy chọn `--add` vì tập tin chưa tồn tại trong khu vực tổ chức của bạn (bạn thậm chí chưa có một khu vực tổ chức được thiết lập) và `--cacheinfo` vì tập tin bạn đang thêm không có trong thư mục của bạn mà có trong cơ sở dữ liệu của bạn.
Sau đó, bạn chỉ định chế độ, SHA-1, và tên tập tin:
[source,console]
----
$ git update-index --add --cacheinfo 100644 \
83baae61804e65cc73a7201a7252750c76066a30 test.txt
----
Trong trường hợp này, bạn đang chỉ định chế độ `100644`, có nghĩa là đó là một tập tin bình thường.
Các tùy chọn khác là `100755`, có nghĩa là đó là một tập tin thực thi; và `120000`, chỉ định một liên kết tượng trưng.
Chế độ được lấy từ các chế độ UNIX bình thường nhưng linh hoạt hơn nhiều -- ba chế độ này là những chế độ duy nhất hợp lệ cho các tập tin (blob) trong Git (mặc dù các chế độ khác được sử dụng cho các thư mục và submodule).
Bây giờ, bạn có thể sử dụng `git write-tree` để ghi khu vực tổ chức ra một đối tượng tree.
Không cần tùy chọn `-w` -- gọi lệnh này tự động tạo một đối tượng tree từ trạng thái của index nếu tree đó chưa tồn tại:
[source,console]
----
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
----
Bạn cũng có thể xác minh rằng đây là một đối tượng tree bằng cách sử dụng cùng lệnh `git cat-file` mà bạn đã thấy trước đó:
[source,console]
----
$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree
----
Bây giờ bạn sẽ tạo một tree mới với phiên bản thứ hai của `test.txt` và một tập tin mới:
[source,console]
----
$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 \
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
----
Khu vực tổ chức của bạn bây giờ có phiên bản mới của `test.txt` cũng như tập tin mới `new.txt`.
Ghi ra tree đó (ghi lại trạng thái của khu vực tổ chức hoặc index vào một đối tượng tree) và xem nó trông như thế nào:
[source,console]
----
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
----
Lưu ý rằng tree này có cả hai mục tập tin và cũng rằng SHA-1 của `test.txt` là SHA-1 "`version 2`" từ trước đó (`1f7a7a`).
Chỉ để cho vui, bạn sẽ thêm tree đầu tiên như một thư mục con vào tree này.
Bạn có thể đọc các tree vào khu vực tổ chức của mình bằng cách gọi `git read-tree`.
Trong trường hợp này, bạn có thể đọc một tree hiện có vào khu vực tổ chức của mình như một subtree bằng cách sử dụng tùy chọn `--prefix` với lệnh này:
[source,console]
----
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
----
Nếu bạn tạo một thư mục làm việc từ tree mới mà bạn vừa viết, bạn sẽ nhận được hai tập tin ở cấp cao nhất của thư mục làm việc và một thư mục con có tên `bak` chứa phiên bản đầu tiên của tập tin `test.txt`.
Bạn có thể nghĩ về dữ liệu mà Git chứa cho các cấu trúc này như thế này:
.Cấu trúc nội dung của dữ liệu Git hiện tại của bạn
image::images/data-model-2.png[Cấu trúc nội dung của dữ liệu Git hiện tại của bạn]
[[_git_commit_objects]]
==== Đối tượng Commit
Nếu bạn đã làm tất cả những điều trên, bây giờ bạn có ba tree đại diện cho các ảnh chụp nhanh khác nhau của dự án mà bạn muốn theo dõi, nhưng vấn đề trước đó vẫn còn: bạn phải nhớ cả ba giá trị SHA-1 để thu hồi các ảnh chụp nhanh.
Bạn cũng không có bất kỳ thông tin nào về ai đã lưu các ảnh chụp nhanh, khi nào chúng được lưu, hoặc tại sao chúng được lưu.
Đây là thông tin cơ bản mà đối tượng commit lưu trữ cho bạn.
Để tạo một đối tượng commit, bạn gọi `commit-tree` và chỉ định một SHA-1 tree duy nhất và các đối tượng commit nào, nếu có, trực tiếp đứng trước nó.
Bắt đầu với tree đầu tiên bạn đã viết:
[source,console]
----
$ echo 'First commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
----
[NOTE]
====
Bạn sẽ nhận được một giá trị băm khác nhau vì thời gian tạo và dữ liệu tác giả khác nhau.
Hơn nữa, mặc dù về nguyên tắc bất kỳ đối tượng commit nào cũng có thể được tái tạo chính xác với dữ liệu đó, các chi tiết lịch sử về cấu trúc của cuốn sách này có nghĩa là các hàm băm commit được in có thể không tương ứng với các commit đã cho.
Thay thế các hàm băm commit và tag bằng tổng kiểm tra của riêng bạn trong chương này.
====
Bây giờ bạn có thể xem đối tượng commit mới của mình bằng `git cat-file`:
[source,console]
----
$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700
First commit
----
Định dạng cho một đối tượng commit rất đơn giản: nó chỉ định tree cấp cao nhất cho ảnh chụp nhanh của dự án tại thời điểm đó; các commit cha mẹ nếu có (đối tượng commit được mô tả ở trên không có bất kỳ cha mẹ nào); thông tin tác giả/người commit (sử dụng các thiết lập cấu hình `user.name` và `user.email` của bạn và một dấu thời gian); một dòng trống, và sau đó là thông điệp commit.
Tiếp theo, bạn sẽ viết hai đối tượng commit khác, mỗi cái tham chiếu đến commit đến trực tiếp trước nó:
[source,console]
----
$ echo 'Second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'Third commit' | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9
----
Mỗi trong ba đối tượng commit trỏ đến một trong ba tree ảnh chụp nhanh mà bạn đã tạo.
Kỳ lạ thay, bây giờ bạn có một lịch sử Git thực sự mà bạn có thể xem bằng lệnh `git log`, nếu bạn chạy nó trên SHA-1 commit cuối cùng:
[source,console]
----
$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
Third commit
bak/test.txt | 1 +
1 file changed, 1 insertion(+)
commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:14:29 2009 -0700
Second commit
new.txt | 1 +
test.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:09:34 2009 -0700
First commit
test.txt | 1 +
1 file changed, 1 insertion(+)
----
Tuyệt vời.
Bạn vừa thực hiện các thao tác cấp thấp để xây dựng một lịch sử Git mà không sử dụng bất kỳ lệnh giao diện người dùng nào.
Đây về cơ bản là những gì Git làm khi bạn chạy các lệnh `git add` và `git commit` -- nó lưu trữ các blob cho các tập tin đã thay đổi, cập nhật index, ghi ra các tree, và ghi các đối tượng commit tham chiếu đến các tree cấp cao nhất và các commit đến ngay trước chúng.
Ba đối tượng Git chính này -- blob, tree, và commit -- ban đầu được lưu trữ dưới dạng các tập tin riêng biệt trong thư mục `.git/objects` của bạn.
Đây là tất cả các đối tượng trong thư mục ví dụ bây giờ, được chú thích với những gì chúng lưu trữ:
[source,console]
----
$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1
----
Nếu bạn theo dõi tất cả các con trỏ nội bộ, bạn sẽ có một đồ thị đối tượng giống như thế này:
.Tất cả các đối tượng có thể truy cập được trong thư mục Git của bạn
image::images/data-model-3.png[Tất cả các đối tượng có thể truy cập được trong thư mục Git của bạn]
==== Lưu trữ Đối tượng
Chúng tôi đã đề cập trước đó rằng có một tiêu đề được lưu trữ với mỗi đối tượng bạn commit vào cơ sở dữ liệu đối tượng Git của mình.
Hãy dành một phút để xem Git lưu trữ các đối tượng của nó như thế nào.
Bạn sẽ thấy cách lưu trữ một đối tượng blob -- trong trường hợp này, chuỗi "`what is up, doc?`" -- một cách tương tác trong ngôn ngữ kịch bản Ruby.
Bạn có thể khởi động chế độ Ruby tương tác bằng lệnh `irb`:
[source,console]
----
$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"
----
Git đầu tiên xây dựng một tiêu đề bắt đầu bằng việc xác định loại đối tượng -- trong trường hợp này, là một blob.
Đối với phần đầu tiên của tiêu đề, Git thêm một khoảng trắng theo sau là kích thước tính bằng byte của nội dung, và thêm một byte null cuối cùng:
[source,console]
----
>> header = "blob #{content.bytesize}\0"
=> "blob 16\u0000"
----
Git nối tiêu đề và nội dung gốc và sau đó tính toán tổng kiểm tra SHA-1 của nội dung mới đó.
Bạn có thể tính toán giá trị SHA-1 của một chuỗi trong Ruby bằng cách bao gồm thư viện digest SHA1 với lệnh `require` và sau đó gọi `Digest::SHA1.hexdigest()` với chuỗi:
[source,console]
----
>> store = header + content
=> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"
----
Hãy so sánh điều đó với đầu ra của `git hash-object`.
Ở đây chúng ta sử dụng `echo -n` để ngăn thêm một dòng mới vào đầu vào.
[source,console]
----
$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37
----
Git nén nội dung mới với zlib, mà bạn có thể làm trong Ruby với thư viện zlib.
Đầu tiên, bạn cần require thư viện và sau đó chạy `Zlib::Deflate.deflate()` trên nội dung:
[source,console]
----
>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"
----
Cuối cùng, bạn sẽ ghi nội dung đã được nén zlib của mình vào một đối tượng trên đĩa.
Bạn sẽ xác định đường dẫn của đối tượng bạn muốn ghi ra (hai ký tự đầu tiên của giá trị SHA-1 là tên thư mục con, và 38 ký tự cuối cùng là tên tập tin trong thư mục đó).
Trong Ruby, bạn có thể sử dụng hàm `FileUtils.mkdir_p()` để tạo thư mục con nếu nó không tồn tại.
Sau đó, mở tập tin bằng `File.open()` và ghi nội dung đã được nén zlib trước đó vào tập tin bằng một lệnh gọi `write()` trên handle tập tin kết quả:
[source,console]
----
>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32
----
Hãy kiểm tra nội dung của đối tượng bằng cách sử dụng `git cat-file`:
[source,console]
---
$ git cat-file -p bd9dbf5aae1a3862dd1526723246b20206e5fc37
what is up, doc?
---
Vậy là xong – bạn đã tạo một đối tượng blob Git hợp lệ.
Tất cả các đối tượng Git được lưu trữ theo cùng một cách, chỉ với các loại khác nhau – thay vì chuỗi blob, tiêu đề sẽ bắt đầu bằng commit hoặc tree.
Ngoài ra, mặc dù nội dung blob có thể là gần như bất cứ thứ gì, nội dung commit và tree được định dạng rất cụ thể.
[[_git_refs]]
=== Tham chiếu Git
Nếu bạn quan tâm đến việc xem lịch sử của kho chứa có thể truy cập được từ commit, chẳng hạn, `1a410e`, bạn có thể chạy một cái gì đó như `git log 1a410e` để hiển thị lịch sử đó, nhưng bạn vẫn phải nhớ rằng `1a410e` là commit bạn muốn sử dụng làm điểm bắt đầu cho lịch sử đó.
Thay vào đó, sẽ dễ dàng hơn nếu bạn có một tập tin trong đó bạn có thể lưu trữ giá trị SHA-1 đó dưới một tên đơn giản để bạn có thể sử dụng tên đơn giản đó thay vì giá trị SHA-1 thô.
Trong Git, những tên đơn giản này được gọi là "`references`" (tham chiếu) hoặc "`refs`"; bạn có thể tìm thấy các tập tin chứa những giá trị SHA-1 đó trong thư mục `.git/refs`.
Trong dự án hiện tại, thư mục này không chứa tập tin nào, nhưng nó chứa một cấu trúc đơn giản:
[source,console]
----
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs -type f
----
Để tạo một tham chiếu mới sẽ giúp bạn nhớ commit mới nhất của mình ở đâu, về mặt kỹ thuật bạn có thể làm một cái gì đó đơn giản như thế này:
[source,console]
----
$ echo 1a410efbd13591db07496601ebc7a059dd55cfe9 > .git/refs/heads/master
----
Bây giờ, bạn có thể sử dụng tham chiếu head bạn vừa tạo thay vì giá trị SHA-1 trong các lệnh Git của mình:
[source,console]
----
$ git log --pretty=oneline master
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
----
Bạn không được khuyến khích chỉnh sửa trực tiếp các tập tin tham chiếu; thay vào đó, Git cung cấp lệnh an toàn hơn `git update-ref` để làm điều này nếu bạn muốn cập nhật một tham chiếu:
[source,console]
----
$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9
----
Đó về cơ bản là một nhánh trong Git: một con trỏ đơn giản hoặc tham chiếu đến đầu của một dòng công việc.
Để tạo một nhánh trở lại commit thứ hai, bạn có thể làm điều này:
[source,console]
----
$ git update-ref refs/heads/test cac0ca
----
Nhánh của bạn sẽ chỉ chứa công việc từ commit đó trở xuống:
[source,console]
----
$ git log --pretty=oneline test
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
----
Bây giờ, cơ sở dữ liệu Git của bạn về mặt khái niệm trông giống như thế này:
.Các đối tượng thư mục Git với các tham chiếu đầu nhánh được bao gồm
image::images/data-model-4.png[Các đối tượng thư mục Git với các tham chiếu đầu nhánh được bao gồm]
Khi bạn chạy các lệnh như `git branch <branch>`, Git về cơ bản chạy lệnh `update-ref` đó để thêm SHA-1 của commit cuối cùng của nhánh bạn đang ở vào bất kỳ tham chiếu mới nào bạn muốn tạo.
[[ref_the_ref]]
==== HEAD
Câu hỏi bây giờ là, khi bạn chạy `git branch <branch>`, Git biết SHA-1 của commit cuối cùng như thế nào?
Câu trả lời là tập tin HEAD.
Thường thì tập tin HEAD là một tham chiếu tượng trưng (symbolic reference) đến nhánh bạn hiện đang ở.
Bằng tham chiếu tượng trưng, chúng tôi có nghĩa là không giống như một tham chiếu bình thường, nó chứa một con trỏ đến một tham chiếu khác.
Tuy nhiên trong một số trường hợp hiếm, tập tin HEAD có thể chứa giá trị SHA-1 của một đối tượng Git.
Điều này xảy ra khi bạn checkout một tag, commit, hoặc nhánh từ xa, điều này đặt kho chứa của bạn vào trạng thái https://git-scm.com/docs/git-checkout#_detached_head["detached HEAD"^].
Nếu bạn nhìn vào tập tin, bạn thường sẽ thấy một cái gì đó như thế này:
[source,console]
----
$ cat .git/HEAD
ref: refs/heads/master
----
Nếu bạn chạy `git checkout test`, Git cập nhật tập tin để trông như thế này:
[source,console]
----
$ cat .git/HEAD
ref: refs/heads/test
----
Khi bạn chạy `git commit`, nó tạo đối tượng commit, chỉ định cha mẹ của đối tượng commit đó là bất kỳ giá trị SHA-1 nào mà tham chiếu trong HEAD trỏ đến.
Bạn cũng có thể chỉnh sửa thủ công tập tin này, nhưng một lần nữa một lệnh an toàn hơn tồn tại để làm như vậy: `git symbolic-ref`.
Bạn có thể đọc giá trị của HEAD của mình thông qua lệnh này:
[source,console]
----
$ git symbolic-ref HEAD
refs/heads/master
----
Bạn cũng có thể đặt giá trị của HEAD bằng cách sử dụng cùng một lệnh:
[source,console]
----
$ git symbolic-ref HEAD refs/heads/test
$ cat .git/HEAD
ref: refs/heads/test
----
Bạn không thể đặt một tham chiếu tượng trưng bên ngoài kiểu refs:
[source,console]
----
$ git symbolic-ref HEAD test
fatal: Refusing to point HEAD outside of refs/
----
==== Tags
Chúng ta vừa hoàn thành việc thảo luận về ba loại đối tượng chính của Git (_blobs_, _trees_ và _commits_), nhưng có một loại thứ tư.
Đối tượng _tag_ rất giống như một đối tượng commit -- nó chứa một người gắn tag, một ngày, một thông điệp, và một con trỏ.
Sự khác biệt chính là một đối tượng tag thường trỏ đến một commit thay vì một tree.
Nó giống như một tham chiếu nhánh, nhưng nó không bao giờ di chuyển -- nó luôn trỏ đến cùng một commit nhưng đặt cho nó một tên thân thiện hơn.
Như đã thảo luận trong <<ch02-git-basics-chapter#ch02-git-basics-chapter>>, có hai loại tag: annotated và lightweight.
Bạn có thể tạo một tag lightweight bằng cách chạy một cái gì đó như thế này:
[source,console]
----
$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d
----
Đó là tất cả những gì một tag lightweight là -- một tham chiếu không bao giờ di chuyển.
Tuy nhiên, một tag annotated phức tạp hơn.
Nếu bạn tạo một tag annotated, Git tạo một đối tượng tag và sau đó ghi một tham chiếu để trỏ đến nó thay vì trực tiếp đến commit.
Bạn có thể thấy điều này bằng cách tạo một tag annotated (sử dụng tùy chọn `-a`):
[source,console]
----
$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'Test tag'
----
Đây là giá trị SHA-1 của đối tượng mà nó đã tạo:
[source,console]
----
$ cat .git/refs/tags/v1.1
9585191f37f7b0fb9444f35a9bf50de191beadc2
----
Bây giờ, chạy `git cat-file -p` trên giá trị SHA-1 đó:
[source,console]
----
$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
object 1a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1.1
tagger Scott Chacon <schacon@gmail.com> Sat May 23 16:48:58 2009 -0700
Test tag
----
Lưu ý rằng mục object trỏ đến giá trị SHA-1 của commit mà bạn đã gắn tag.
Cũng lưu ý rằng nó không cần phải trỏ đến một commit; bạn có thể gắn tag bất kỳ đối tượng Git nào.
Trong mã nguồn Git, ví dụ, người bảo trì đã thêm khóa công khai GPG của họ dưới dạng một đối tượng blob và sau đó gắn tag nó.
Bạn có thể xem khóa công khai bằng cách chạy lệnh này trong một bản sao của kho chứa Git:
[source,console]
----
$ git cat-file blob junio-gpg-pub
----
Kho chứa kernel Linux cũng có một đối tượng tag không trỏ đến commit -- tag đầu tiên được tạo trỏ đến tree ban đầu của việc nhập mã nguồn.
==== Remotes
Loại tham chiếu thứ ba mà bạn sẽ thấy là một tham chiếu từ xa (remote reference).
Nếu bạn thêm một remote và đẩy lên nó, Git lưu trữ giá trị bạn đã đẩy lên remote đó lần cuối cho mỗi nhánh trong thư mục `refs/remotes`.
Ví dụ, bạn có thể thêm một remote có tên `origin` và đẩy nhánh `master` của bạn lên nó:
[source,console]
----
$ git remote add origin git@github.com:schacon/simplegit-progit.git
$ git push origin master
Counting objects: 11, done.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 716 bytes, done.
Total 7 (delta 2), reused 4 (delta 1)
To git@github.com:schacon/simplegit-progit.git
a11bef0..ca82a6d master -> master
----
Sau đó, bạn có thể thấy nhánh `master` trên remote `origin` là gì lần cuối cùng bạn giao tiếp với máy chủ, bằng cách kiểm tra tập tin `refs/remotes/origin/master`:
[source,console]
----
$ cat .git/refs/remotes/origin/master
ca82a6dff817ec66f44342007202690a93763949
----
Các tham chiếu từ xa khác với các nhánh (tham chiếu `refs/heads`) chủ yếu ở chỗ chúng được coi là chỉ đọc.
Bạn có thể `git checkout` đến một cái, nhưng Git sẽ không tham chiếu tượng trưng HEAD đến một cái, vì vậy bạn sẽ không bao giờ cập nhật nó bằng lệnh `commit`.
Git quản lý chúng như các dấu trang đến trạng thái đã biết cuối cùng của nơi các nhánh đó ở trên các máy chủ đó.
=== Packfiles
Nếu bạn đã làm theo tất cả các hướng dẫn trong ví dụ từ phần trước, bây giờ bạn nên có một kho chứa Git thử nghiệm với 11 đối tượng -- bốn blob, ba tree, ba commit, và một tag:
[source,console]
----
$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1
----
Git nén nội dung của các tập tin này bằng zlib, và bạn không lưu trữ nhiều, vì vậy tất cả các tập tin này cộng lại chỉ chiếm 925 byte.
Bây giờ bạn sẽ thêm một số nội dung có kích thước lớn hơn vào kho chứa để minh họa một tính năng thú vị của Git.
Để minh họa, chúng ta sẽ thêm tập tin `repo.rb` từ thư viện Grit -- đây là một tập tin mã nguồn khoảng 22K:
[source,console]
----
$ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb
$ git checkout master
$ git add repo.rb
$ git commit -m 'Create repo.rb'
[master 484a592] Create repo.rb
3 files changed, 709 insertions(+), 2 deletions(-)
delete mode 100644 bak/test.txt
create mode 100644 repo.rb
rewrite test.txt (100%)
----
Nếu bạn nhìn vào tree kết quả, bạn có thể thấy giá trị SHA-1 đã được tính toán cho đối tượng blob `repo.rb` mới của bạn:
[source,console]
----
$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt
----
Sau đó bạn có thể sử dụng `git cat-file` để xem đối tượng đó lớn như thế nào:
[source,console]
----
$ git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044
----
Tại thời điểm này, sửa đổi tập tin đó một chút, và xem điều gì xảy ra:
[source,console]
----
$ echo '# testing' >> repo.rb
$ git commit -am 'Modify repo.rb a bit'
[master 2431da6] Modify repo.rb a bit
1 file changed, 1 insertion(+)
----
Kiểm tra tree được tạo bởi commit cuối cùng đó, và bạn thấy một cái gì đó thú vị:
[source,console]
----
$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt
----
Blob bây giờ là một blob khác, có nghĩa là mặc dù bạn chỉ thêm một dòng duy nhất vào cuối một tập tin 400 dòng, Git đã lưu trữ nội dung mới đó như một đối tượng hoàn toàn mới:
[source,console]
----
$ git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
22054
----
Bạn có hai đối tượng gần như giống hệt nhau 22K trên đĩa của mình (mỗi cái được nén xuống khoảng 7K).
Sẽ không tốt nếu Git có thể lưu trữ một trong số chúng đầy đủ nhưng sau đó đối tượng thứ hai chỉ là delta giữa nó và cái đầu tiên?
Hóa ra là nó có thể.
Định dạng ban đầu mà Git lưu các đối tượng trên đĩa được gọi là định dạng đối tượng "`loose`" (lỏng lẻo).
Tuy nhiên, thỉnh thoảng Git đóng gói một số đối tượng này vào một tập tin nhị phân duy nhất gọi là "`packfile`" để tiết kiệm không gian và hiệu quả hơn.
Git làm điều này nếu bạn có quá nhiều đối tượng loose xung quanh, nếu bạn chạy lệnh `git gc` thủ công, hoặc nếu bạn đẩy lên một máy chủ từ xa.
Để xem điều gì xảy ra, bạn có thể yêu cầu Git đóng gói các đối tượng một cách thủ công bằng cách gọi lệnh `git gc`:
[source,console]
----
$ git gc
Counting objects: 18, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (18/18), done.
Total 18 (delta 3), reused 0 (delta 0)
----
Nếu bạn nhìn vào thư mục `objects` của mình, bạn sẽ thấy rằng hầu hết các đối tượng của bạn đã biến mất, và một cặp tập tin mới đã xuất hiện:
[source,console]
----
$ find .git/objects -type f
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack
----
Các đối tượng còn lại là các blob không được trỏ đến bởi bất kỳ commit nào -- trong trường hợp này, các blob ví dụ "`what is up, doc?`" và "`test content`" mà bạn đã tạo trước đó.
Vì bạn chưa bao giờ thêm chúng vào bất kỳ commit nào, chúng được coi là dangling (lơ lửng) và không được đóng gói trong packfile mới của bạn.
Các tập tin khác là packfile mới của bạn và một index.
Packfile là một tập tin duy nhất chứa nội dung của tất cả các đối tượng đã bị xóa khỏi hệ thống tập tin của bạn.
Index là một tập tin chứa các offset vào packfile đó để bạn có thể nhanh chóng tìm kiếm đến một đối tượng cụ thể.
Điều thú vị là mặc dù các đối tượng trên đĩa trước khi bạn chạy lệnh `gc` có kích thước tổng cộng khoảng 15K, packfile mới chỉ có 7K.
Bạn đã cắt giảm việc sử dụng đĩa của mình xuống một nửa bằng cách đóng gói các đối tượng của bạn.
Git làm điều này như thế nào?
Khi Git đóng gói các đối tượng, nó tìm kiếm các tập tin được đặt tên và có kích thước tương tự, và chỉ lưu trữ các delta từ một phiên bản của tập tin sang phiên bản tiếp theo.
Bạn có thể nhìn vào packfile và xem Git đã làm gì để tiết kiệm không gian.
Lệnh plumbing `git verify-pack` cho phép bạn xem những gì đã được đóng gói:
[source,console]
----
$ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
43168a18b7613d1281e5560855a83eb8fde3d687 commit 213 146 464
092917823486a802e94d727c820a9024e14a1fc2 commit 214 146 610
702470739ce72005e2edff522fde85d52a65df9b commit 165 118 756
d368d0ac0678cbe6cce505be58126d3526706e54 tag 130 122 874
fe879577cb8cffcdf25441725141e310dd7d239b tree 136 136 996
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree 36 46 1132
deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree 136 136 1178
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree 6 17 1314 1 \
deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree 8 19 1331 1 \
deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155eb4229851634a0f03eb265b69f5a2d56f341 tree 71 76 1350
83baae61804e65cc73a7201a7252750c76066a30 blob 10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob 9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob 22054 5799 1463
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 9 20 7262 1 \
b042a60ef7dff760008df33cee372b945b6e884e
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 10 19 7282
non delta: 15 objects
chain length = 1: 3 objects
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok
----
Ở đây, blob `033b4`, nếu bạn nhớ là phiên bản đầu tiên của tập tin `repo.rb` của bạn, đang tham chiếu đến blob `b042a`, là phiên bản thứ hai của tập tin.
Cột thứ ba trong đầu ra là kích thước của đối tượng trong pack, vì vậy bạn có thể thấy rằng `b042a` chiếm 22K của tập tin, nhưng `033b4` chỉ chiếm 9 byte.
Điều cũng thú vị là phiên bản thứ hai của tập tin là phiên bản được lưu trữ nguyên vẹn, trong khi phiên bản gốc được lưu trữ dưới dạng delta -- điều này là vì bạn rất có thể cần truy cập nhanh hơn vào phiên bản gần đây nhất của tập tin.
Điều thực sự tốt đẹp về điều này là nó có thể được đóng gói lại bất cứ lúc nào.
Git thỉnh thoảng sẽ tự động đóng gói lại cơ sở dữ liệu của bạn, luôn cố gắng tiết kiệm nhiều không gian hơn, nhưng bạn cũng có thể đóng gói lại thủ công bất cứ lúc nào bằng cách chạy `git gc` bằng tay.
[[_refspec]]
=== Refspec
Trong suốt cuốn sách này, chúng ta đã sử dụng các ánh xạ đơn giản từ các nhánh từ xa đến các tham chiếu cục bộ, nhưng chúng có thể phức tạp hơn.
Giả sử bạn đang theo dõi với một vài phần cuối cùng và đã tạo một kho chứa Git cục bộ nhỏ, và bây giờ muốn thêm một _remote_ vào nó:
[source,console]
----
$ git remote add origin https://github.com/schacon/simplegit-progit
----
Chạy lệnh trên sẽ thêm một phần vào tập tin `.git/config` của kho chứa, chỉ định tên của remote (`origin`), URL của kho chứa từ xa, và _refspec_ được sử dụng để lấy:
[source,ini]
----
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
----
Định dạng của refspec là, đầu tiên, một `+` tùy chọn, theo sau là `<src>:<dst>`, trong đó `<src>` là mẫu cho các tham chiếu ở phía từ xa và `<dst>` là nơi các tham chiếu đó sẽ được theo dõi cục bộ.
`+` bảo Git cập nhật tham chiếu ngay cả khi nó không phải là một fast-forward.
Trong trường hợp mặc định được tự động viết bởi lệnh `git remote add origin`, Git lấy tất cả các tham chiếu dưới `refs/heads/` trên máy chủ và ghi chúng vào `refs/remotes/origin/` cục bộ.
Vì vậy, nếu có một nhánh `master` trên máy chủ, bạn có thể truy cập nhật ký của nhánh đó cục bộ thông qua bất kỳ cái nào sau đây:
[source,console]
----
$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master
----
Tất cả đều tương đương, vì Git mở rộng mỗi cái trong số chúng thành `refs/remotes/origin/master`.
Nếu bạn muốn Git thay vào đó chỉ kéo xuống nhánh `master` mỗi lần, và không phải mọi nhánh khác trên máy chủ từ xa, bạn có thể thay đổi dòng fetch để chỉ tham chiếu đến nhánh đó:
[source]
----
fetch = +refs/heads/master:refs/remotes/origin/master
----
Đây chỉ là refspec mặc định cho `git fetch` cho remote đó.
Nếu bạn muốn thực hiện một lần lấy duy nhất, bạn cũng có thể chỉ định refspec cụ thể trên dòng lệnh.
Để kéo nhánh `master` trên remote xuống `origin/mymaster` cục bộ, bạn có thể chạy:
[source,console]
----
$ git fetch origin master:refs/remotes/origin/mymaster
----
Bạn cũng có thể chỉ định nhiều refspec.
Trên dòng lệnh, bạn có thể kéo xuống một số nhánh như thế này:
[source,console]
----
$ git fetch origin master:refs/remotes/origin/mymaster \
topic:refs/remotes/origin/topic
From git@github.com:schacon/simplegit
! [rejected] master -> origin/mymaster (non fast forward)
* [new branch] topic -> origin/topic
----
Trong trường hợp này, việc kéo nhánh `master` đã bị từ chối vì nó không được liệt kê là một tham chiếu fast-forward.
Bạn có thể ghi đè điều đó bằng cách chỉ định `+` ở phía trước refspec.
Bạn cũng có thể chỉ định nhiều refspec để lấy trong tập tin cấu hình của mình.
Nếu bạn muốn luôn lấy các nhánh `master` và `experiment` từ remote `origin`, hãy thêm hai dòng:
[source,ini]
----
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/experiment:refs/remotes/origin/experiment
----
Kể từ Git 2.6.0, bạn có thể sử dụng các glob một phần trong mẫu để khớp với nhiều nhánh, vì vậy điều này hoạt động:
[source,ini]
----
fetch = +refs/heads/qa*:refs/remotes/origin/qa*
----
Thậm chí tốt hơn, bạn có thể sử dụng namespace (hoặc thư mục) để hoàn thành điều tương tự với cấu trúc nhiều hơn.
Nếu bạn có một nhóm QA đẩy một loạt các nhánh, và bạn muốn lấy nhánh `master` và bất kỳ nhánh nào của nhóm QA nhưng không có gì khác, bạn có thể sử dụng một phần cấu hình như thế này:
[source,ini]
----
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/qa/*:refs/remotes/origin/qa/*
----
Nếu bạn có một quy trình làm việc phức tạp có một nhóm QA đẩy các nhánh, các nhà phát triển đẩy các nhánh, và các nhóm tích hợp đẩy và cộng tác trên các nhánh từ xa, bạn có thể đặt namespace cho chúng dễ dàng theo cách này.
[[_pushing_refspecs]]
==== Đẩy Refspec
Thật tốt khi bạn có thể lấy các tham chiếu có namespace theo cách đó, nhưng làm thế nào nhóm QA đưa các nhánh của họ vào namespace `qa/` ngay từ đầu?
Bạn hoàn thành điều đó bằng cách sử dụng refspec để đẩy.
Nếu nhóm QA muốn đẩy nhánh `master` của họ lên `qa/master` trên máy chủ từ xa, họ có thể chạy:
[source,console]
----
$ git push origin master:refs/heads/qa/master
----
Nếu họ muốn Git tự động làm điều đó mỗi khi họ chạy `git push origin`, họ có thể thêm một giá trị `push` vào tập tin cấu hình của họ:
[source,ini]
----
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
push = refs/heads/master:refs/heads/qa/master
----
Một lần nữa, điều này sẽ khiến `git push origin` đẩy nhánh `master` cục bộ lên nhánh `qa/master` từ xa theo mặc định.
[NOTE]
====
Bạn không thể sử dụng refspec để lấy từ một kho chứa và đẩy đến một kho chứa khác.
Để biết ví dụ làm như vậy, hãy tham khảo <<ch06-github#_fetch_and_push_on_different_repositories>>.
====
==== Xóa Tham chiếu
Bạn cũng có thể sử dụng refspec để xóa các tham chiếu khỏi máy chủ từ xa bằng cách chạy một cái gì đó như thế này:
[source,console]
----
$ git push origin :topic
----
Vì refspec là `<src>:<dst>`, bằng cách bỏ phần `<src>`, điều này về cơ bản nói là làm cho nhánh `topic` trên remote không có gì, điều này xóa nó.
Hoặc bạn có thể sử dụng cú pháp mới hơn (có sẵn kể từ Git v1.7.0):
[source,console]
----
$ git push origin --delete topic
----
=== Giao thức Truyền tải
Git có thể truyền dữ liệu giữa hai kho chứa theo hai cách chính: giao thức "`dumb`" (ngu) và giao thức "`smart`" (thông minh).
Phần này sẽ nhanh chóng đề cập đến cách hai giao thức chính này hoạt động.
==== Giao thức Dumb
Nếu bạn đang thiết lập một kho chứa để được phục vụ chỉ đọc qua HTTP, giao thức dumb có thể sẽ được sử dụng.
Giao thức này được gọi là "`dumb`" vì nó không yêu cầu mã cụ thể cho Git ở phía máy chủ trong quá trình truyền tải; quá trình lấy là một loạt các yêu cầu HTTP `GET`, trong đó máy khách có thể giả định bố cục của kho chứa Git trên máy chủ.
[NOTE]
====
Giao thức dumb hiếm khi được sử dụng ngày nay.
Rất khó để bảo mật hoặc làm riêng tư, vì vậy hầu hết các máy chủ Git (cả dựa trên đám mây và tại chỗ) sẽ từ chối sử dụng nó.
Thường được khuyến nghị sử dụng giao thức smart, mà chúng ta mô tả thêm một chút sau.
====
Hãy theo dõi quá trình `http-fetch` cho thư viện simplegit:
[source,console]
----
$ git clone http://server/simplegit-progit.git
----
Điều đầu tiên lệnh này làm là kéo xuống tập tin `info/refs`.
Tập tin này được viết bởi lệnh `update-server-info`, đó là lý do tại sao bạn cần bật nó như một móc `post-receive` để truyền tải HTTP hoạt động đúng cách:
[source]
----
=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949 refs/heads/master
----
Bây giờ bạn có một danh sách các tham chiếu từ xa và SHA-1.
Tiếp theo, bạn tìm kiếm tham chiếu HEAD là gì để bạn biết cần check out cái gì khi hoàn thành:
[source]
----
=> GET HEAD
ref: refs/heads/master
----
Bạn cần check out nhánh `master` khi hoàn thành quá trình.
Tại thời điểm này, bạn đã sẵn sàng để bắt đầu quá trình đi bộ (walking).
Vì điểm bắt đầu của bạn là đối tượng commit `ca82a6` mà bạn đã thấy trong tập tin `info/refs`, bạn bắt đầu bằng cách lấy nó:
[source]
----
=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)
----
Bạn nhận lại một đối tượng – đối tượng đó ở định dạng loose trên máy chủ, và bạn đã lấy nó qua một yêu cầu HTTP GET tĩnh.
Bạn có thể giải nén zlib, loại bỏ tiêu đề, và xem nội dung commit:
[source,console]
----
$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
Change version number
----
Tiếp theo, bạn có thêm hai đối tượng để truy xuất – `cfda3b`, là tree nội dung mà commit chúng ta vừa truy xuất trỏ đến; và `085bb3`, là commit cha mẹ:
[source]
----
=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)
----
Điều đó cung cấp cho bạn đối tượng commit tiếp theo của bạn.
Lấy đối tượng tree:
[source]
----
=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)
----
Ối – có vẻ như đối tượng tree đó không ở định dạng loose trên máy chủ, vì vậy bạn nhận lại phản hồi 404.
Có một vài lý do cho điều này – đối tượng có thể ở trong một kho chứa thay thế, hoặc nó có thể ở trong một packfile trong kho chứa này.
Git kiểm tra bất kỳ thay thế nào được liệt kê trước:
[source]
----
=> GET objects/info/http-alternates
(empty file)
----
Nếu điều này trả về với một danh sách các URL thay thế, Git kiểm tra các tập tin loose và packfile ở đó – đây là một cơ chế tốt đẹp cho các dự án là fork của nhau để chia sẻ các đối tượng trên đĩa.
Tuy nhiên, vì không có thay thế nào được liệt kê trong trường hợp này, đối tượng của bạn phải ở trong một packfile.
Để xem packfile nào có sẵn trên máy chủ này, bạn cần lấy tập tin `objects/info/packs`, chứa danh sách chúng (cũng được tạo bởi `update-server-info`):
[source]
----
=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
----
Chỉ có một packfile trên máy chủ, vì vậy đối tượng của bạn rõ ràng là ở đó, nhưng bạn sẽ kiểm tra tập tin index để chắc chắn.
Điều này cũng hữu ích nếu bạn có nhiều packfile trên máy chủ, vì vậy bạn có thể thấy packfile nào chứa đối tượng bạn cần:
[source]
----
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)
----
Bây giờ bạn đã có index packfile, bạn có thể thấy đối tượng của mình có trong đó không – vì index liệt kê các SHA-1 của các đối tượng chứa trong packfile và các offset đến các đối tượng đó.
Đối tượng của bạn ở đó, vì vậy hãy tiếp tục và lấy toàn bộ packfile:
[source]
----
=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)
----
Bạn có đối tượng tree của mình, vì vậy bạn tiếp tục đi bộ các commit của mình.
Tất cả chúng cũng nằm trong packfile bạn vừa tải xuống, vì vậy bạn không phải thực hiện thêm bất kỳ yêu cầu nào đến máy chủ của mình.
Git check out một bản sao làm việc của nhánh `master` được trỏ đến bởi tham chiếu HEAD mà bạn đã tải xuống lúc đầu.
==== Giao thức Smart
Giao thức dumb đơn giản nhưng hơi kém hiệu quả, và nó không thể xử lý việc ghi dữ liệu từ máy khách đến máy chủ.
Giao thức smart là một phương pháp phổ biến hơn để truyền dữ liệu, nhưng nó yêu cầu một quá trình ở đầu từ xa thông minh về Git – nó có thể đọc dữ liệu cục bộ, tìm ra những gì máy khách có và cần, và tạo một packfile tùy chỉnh cho nó.
Có hai bộ quá trình để truyền dữ liệu: một cặp để tải lên dữ liệu và một cặp để tải xuống dữ liệu.
===== Tải lên Dữ liệu
(((git commands, send-pack)))(((git commands, receive-pack)))
Để tải lên dữ liệu đến một quá trình từ xa, Git sử dụng các quá trình `send-pack` và `receive-pack`.
Quá trình `send-pack` chạy trên máy khách và kết nối đến một quá trình `receive-pack` ở phía từ xa.
====== SSH
Ví dụ, giả sử bạn chạy `git push origin master` trong dự án của mình, và `origin` được định nghĩa là một URL sử dụng giao thức SSH.
Git khởi động quá trình `send-pack`, khởi tạo một kết nối qua SSH đến máy chủ của bạn.
Nó cố gắng chạy một lệnh trên máy chủ từ xa thông qua một cuộc gọi SSH trông giống như thế này:
[source,console]
----
$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master□report-status \
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000
----
Lệnh `git-receive-pack` ngay lập tức phản hồi với một dòng cho mỗi tham chiếu mà nó hiện có – trong trường hợp này, chỉ là nhánh `master` và SHA-1 của nó.
Dòng đầu tiên cũng có một danh sách các khả năng của máy chủ (ở đây, `report-status`, `delete-refs`, và một số khác, bao gồm định danh máy khách).
Dữ liệu được truyền theo từng khối (chunk).
Mỗi khối bắt đầu bằng một giá trị hex 4 ký tự chỉ định độ dài của khối (bao gồm 4 byte của chính độ dài).
Các khối thường chứa một dòng dữ liệu duy nhất và một linefeed cuối.
Khối đầu tiên của bạn bắt đầu bằng 00a5, là hệ thập lục phân cho 165, có nghĩa là khối dài 165 byte.
Khối tiếp theo là 0000, có nghĩa là máy chủ đã hoàn thành danh sách tham chiếu của nó.
Bây giờ nó biết trạng thái của máy chủ, quá trình `send-pack` của bạn xác định các commit nào nó có mà máy chủ không có.
Đối với mỗi tham chiếu mà lần đẩy này sẽ cập nhật, quá trình `send-pack` bảo quá trình `receive-pack` thông tin đó.
Ví dụ, nếu bạn đang cập nhật nhánh `master` và thêm một nhánh `experiment`, phản hồi `send-pack` có thể trông giống như thế này:
[source]
----
0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
refs/heads/experiment
0000
----
Git gửi một dòng cho mỗi tham chiếu bạn đang cập nhật với độ dài của dòng, SHA-1 cũ, SHA-1 mới, và tham chiếu đang được cập nhật.
Dòng đầu tiên cũng có các khả năng của máy khách.
Giá trị SHA-1 của tất cả '0' có nghĩa là không có gì ở đó trước đó – vì bạn đang thêm tham chiếu `experiment`.
Nếu bạn đang xóa một tham chiếu, bạn sẽ thấy ngược lại: tất cả '0' ở phía bên phải.
Tiếp theo, máy khách gửi một packfile của tất cả các đối tượng mà máy chủ chưa có.
Cuối cùng, máy chủ phản hồi với một chỉ báo thành công (hoặc thất bại):
[source]
----
000eunpack ok
----
====== HTTP(S)
Quá trình này hầu như giống nhau qua HTTP, mặc dù bắt tay hơi khác một chút.
Kết nối được khởi tạo với yêu cầu này:
[source]
----
=> GET http://server/simplegit-progit.git/info/refs?service=git-receive-pack
001f# service=git-receive-pack
00ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master□report-status \
delete-refs side-band-64k quiet ofs-delta \
agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000
----
Đó là kết thúc của trao đổi máy khách-máy chủ đầu tiên.
Sau đó máy khách thực hiện một yêu cầu khác, lần này là một `POST`, với dữ liệu mà `send-pack` cung cấp.
[source]
----
=> POST http://server/simplegit-progit.git/git-receive-pack
----
Yêu cầu `POST` bao gồm đầu ra `send-pack` và packfile làm payload của nó.
Sau đó máy chủ chỉ ra thành công hoặc thất bại với phản hồi HTTP của nó.
Hãy nhớ rằng giao thức HTTP có thể bọc dữ liệu này thêm bên trong một mã hóa truyền tải chunked.
===== Tải xuống Dữ liệu
(((git commands, fetch-pack)))(((git commands, upload-pack)))
Khi bạn tải xuống dữ liệu, các quá trình `fetch-pack` và `upload-pack` có liên quan.
Máy khách khởi tạo một quá trình `fetch-pack` kết nối đến một quá trình `upload-pack` ở phía từ xa để thương lượng dữ liệu nào sẽ được truyền xuống.
====== SSH
Nếu bạn đang thực hiện lấy qua SSH, `fetch-pack` chạy một cái gì đó như thế này:
[source,console]
----
$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"
----
Sau khi `fetch-pack` kết nối, `upload-pack` gửi lại một cái gì đó như thế này:
[source]
----
00dfca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000
----
Điều này rất giống với những gì `receive-pack` phản hồi, nhưng các khả năng khác nhau.
Ngoài ra, nó gửi lại những gì HEAD trỏ đến (`symref=HEAD:refs/heads/master`) để máy khách biết cần check out cái gì nếu đây là một bản sao.
Tại thời điểm này, quá trình `fetch-pack` nhìn vào các đối tượng nào nó có và phản hồi với các đối tượng mà nó cần bằng cách gửi "`want`" và sau đó là SHA-1 mà nó muốn.
Nó gửi tất cả các đối tượng mà nó đã có với "`have`" và sau đó là SHA-1.
Ở cuối danh sách này, nó viết "`done`" để khởi tạo quá trình `upload-pack` bắt đầu gửi packfile của dữ liệu mà nó cần:
[source]
----
003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000
----
====== HTTP(S)
Bắt tay cho một thao tác lấy mất hai yêu cầu HTTP.
Đầu tiên là một `GET` đến cùng một endpoint được sử dụng trong giao thức dumb:
[source]
----
=> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
00e7ca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
side-band side-band-64k ofs-delta shallow no-progress include-tag \
multi_ack_detailed no-done symref=HEAD:refs/heads/master \
agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000
----
Điều này rất giống với việc gọi `git-upload-pack` qua một kết nối SSH, nhưng trao đổi thứ hai được thực hiện như một yêu cầu riêng biệt:
[source]
----
=> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000
----
Một lần nữa, đây là cùng một định dạng như trên.
Phản hồi cho yêu cầu này chỉ ra thành công hoặc thất bại, và bao gồm packfile.
==== Tóm tắt Giao thức
Phần này chứa một tổng quan rất cơ bản về các giao thức truyền tải.
Giao thức bao gồm nhiều tính năng khác, chẳng hạn như khả năng `multi_ack` hoặc `side-band`, nhưng việc đề cập đến chúng nằm ngoài phạm vi của cuốn sách này.
Chúng tôi đã cố gắng cung cấp cho bạn cảm giác về sự trao đổi qua lại chung giữa máy khách và máy chủ; nếu bạn cần nhiều kiến thức hơn thế này, bạn có thể sẽ muốn xem mã nguồn Git.
=== Bảo trì và Phục hồi Dữ liệu
Thỉnh thoảng, bạn có thể phải thực hiện một số dọn dẹp – làm cho một kho chứa nhỏ gọn hơn, dọn dẹp một kho chứa đã nhập, hoặc phục hồi công việc bị mất.
Phần này sẽ đề cập đến một số kịch bản này.
[[_git_gc]]
==== Bảo trì
Thỉnh thoảng, Git tự động chạy một lệnh gọi là "`auto gc`".
Hầu hết thời gian, lệnh này không làm gì cả.
Tuy nhiên, nếu có quá nhiều đối tượng loose (các đối tượng không có trong packfile) hoặc quá nhiều packfile, Git khởi chạy một lệnh `git gc` đầy đủ.
"`gc`" là viết tắt của garbage collect (thu gom rác), và lệnh này thực hiện một số việc: nó thu thập tất cả các đối tượng loose và đặt chúng vào packfile, nó hợp nhất các packfile thành một packfile lớn duy nhất, và nó xóa các đối tượng không thể truy cập được từ bất kỳ commit nào và đã vài tháng tuổi.
Bạn có thể chạy `auto gc` thủ công như sau:
[source,console]
----
$ git gc --auto
----
Một lần nữa, điều này thường không làm gì cả.
Bạn phải có khoảng 7.000 đối tượng loose hoặc hơn 50 packfile để Git khởi động một lệnh `gc` thực sự.
Bạn có thể sửa đổi các giới hạn này với các thiết lập cấu hình `gc.auto` và `gc.autopacklimit`, tương ứng.
Điều khác mà `gc` sẽ làm là đóng gói các tham chiếu của bạn vào một tập tin duy nhất.
Giả sử kho chứa của bạn chứa các nhánh và tag sau:
[source,console]
----
$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1
----
Nếu bạn chạy `git gc`, bạn sẽ không còn các tập tin này trong thư mục `refs` nữa.
Git sẽ di chuyển chúng vì lý do hiệu quả vào một tập tin có tên `.git/packed-refs` trông như thế này:
[source,console]
----
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
----
Nếu bạn cập nhật một tham chiếu, Git không chỉnh sửa tập tin này mà thay vào đó ghi một tập tin mới vào `refs/heads`.
Để lấy SHA-1 thích hợp cho một tham chiếu nhất định, Git kiểm tra tham chiếu đó trong thư mục `refs` và sau đó kiểm tra tập tin `packed-refs` như một dự phòng.
Vì vậy, nếu bạn không thể tìm thấy một tham chiếu trong thư mục `refs`, nó có thể ở trong tập tin `packed-refs` của bạn.
Lưu ý dòng cuối cùng của tập tin, bắt đầu bằng `^`.
Điều này có nghĩa là tag ngay phía trên là một tag annotated và dòng đó là commit mà tag annotated trỏ đến.
[[_data_recovery]]
==== Phục hồi Dữ liệu
Tại một số thời điểm trong hành trình Git của bạn, bạn có thể vô tình mất một commit.
Thường thì điều này xảy ra vì bạn force-delete một nhánh có công việc trên đó, và hóa ra bạn muốn nhánh đó sau cùng; hoặc bạn hard-reset một nhánh, do đó từ bỏ các commit mà bạn muốn một cái gì đó từ chúng.
Giả sử điều này xảy ra, làm thế nào bạn có thể lấy lại các commit của mình?
Đây là một ví dụ hard-reset nhánh `master` trong kho chứa thử nghiệm của bạn về một commit cũ hơn và sau đó phục hồi các commit bị mất.
Đầu tiên, hãy xem lại kho chứa của bạn ở thời điểm này:
[source,console]
----
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
----
Bây giờ, di chuyển nhánh `master` trở lại commit giữa:
[source,console]
----
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef Third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
----
Bạn đã mất hai commit hàng đầu một cách hiệu quả – bạn không có nhánh nào từ đó các commit đó có thể truy cập được.
Bạn cần tìm SHA-1 commit mới nhất và sau đó thêm một nhánh trỏ đến nó.
Thủ thuật là tìm SHA-1 commit mới nhất đó – không giống như bạn đã ghi nhớ nó, phải không?
Thường thì, cách nhanh nhất là sử dụng một công cụ gọi là `git reflog`.
Khi bạn đang làm việc, Git âm thầm ghi lại HEAD của bạn là gì mỗi khi bạn thay đổi nó.
Mỗi khi bạn commit hoặc thay đổi nhánh, reflog được cập nhật.
Reflog cũng được cập nhật bởi lệnh `git update-ref`, đó là một lý do khác để sử dụng nó thay vì chỉ ghi giá trị SHA-1 vào các tập tin ref của bạn, như chúng tôi đã đề cập trong <<ch10-git-internals#_git_refs>>.
Bạn có thể thấy bạn đã ở đâu bất cứ lúc nào bằng cách chạy `git reflog`:
[source,console]
----
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: Modify repo.rb a bit
484a592 HEAD@{2}: commit: Create repo.rb
----
Ở đây chúng ta có thể thấy hai commit mà chúng ta đã check out, tuy nhiên không có nhiều thông tin ở đây.
Để xem cùng thông tin theo cách hữu ích hơn nhiều, chúng ta có thể chạy `git log -g`, sẽ cung cấp cho bạn một đầu ra nhật ký bình thường cho reflog của bạn.
[source,console]
----
$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:22:37 2009 -0700
Third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
Modify repo.rb a bit
----
Có vẻ như commit dưới cùng là commit bạn đã mất, vì vậy bạn có thể phục hồi nó bằng cách tạo một nhánh mới tại commit đó.
Ví dụ, bạn có thể bắt đầu một nhánh có tên `recover-branch` tại commit đó (ab1afef):
[source,console]
----
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
----
Tuyệt – bây giờ bạn có một nhánh có tên `recover-branch` ở nơi nhánh `master` của bạn từng ở, làm cho hai commit đầu tiên có thể truy cập được lại.
Tiếp theo, giả sử mất mát của bạn vì một lý do nào đó không có trong reflog – bạn có thể mô phỏng điều đó bằng cách xóa `recover-branch` và xóa reflog.
Bây giờ hai commit đầu tiên không thể truy cập được bởi bất cứ thứ gì:
[source,console]
----
$ git branch -D recover-branch
$ rm -Rf .git/logs/
----
Vì dữ liệu reflog được giữ trong thư mục `.git/logs/`, bạn thực sự không có reflog.
Làm thế nào bạn có thể phục hồi commit đó tại thời điểm này?
Một cách là sử dụng tiện ích `git fsck`, kiểm tra tính toàn vẹn của cơ sở dữ liệu của bạn.
Nếu bạn chạy nó với tùy chọn `--full`, nó hiển thị cho bạn tất cả các đối tượng không được trỏ đến bởi một đối tượng khác:
[source,console]
----
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
----
Trong trường hợp này, bạn có thể thấy commit bị thiếu của mình sau chuỗi "`dangling commit`".
Bạn có thể phục hồi nó theo cách tương tự, bằng cách thêm một nhánh trỏ đến SHA-1 đó.
[[_removing_objects]]
==== Xóa Đối tượng
Có rất nhiều điều tuyệt vời về Git, nhưng một tính năng có thể gây ra vấn đề là thực tế rằng `git clone` tải xuống toàn bộ lịch sử của dự án, bao gồm mọi phiên bản của mọi tập tin.
Điều này tốt nếu toàn bộ là mã nguồn, vì Git được tối ưu hóa cao để nén dữ liệu đó một cách hiệu quả.
Tuy nhiên, nếu ai đó tại bất kỳ thời điểm nào trong lịch sử dự án của bạn đã thêm một tập tin lớn duy nhất, mọi bản sao cho mọi thời gian sẽ bị buộc phải tải xuống tập tin lớn đó, ngay cả khi nó đã bị xóa khỏi dự án trong commit ngay sau đó.
Vì nó có thể truy cập được từ lịch sử, nó sẽ luôn ở đó.
Điều này có thể là một vấn đề lớn khi bạn đang chuyển đổi các kho chứa Subversion hoặc Perforce sang Git.
Vì bạn không tải xuống toàn bộ lịch sử trong các hệ thống đó, loại bổ sung này mang lại ít hậu quả.
Nếu bạn đã thực hiện nhập từ một hệ thống khác hoặc nếu không thì thấy rằng kho chứa của bạn lớn hơn nhiều so với nó nên, đây là cách bạn có thể tìm và xóa các đối tượng lớn.
*Hãy cảnh báo: kỹ thuật này mang tính phá hủy đối với lịch sử commit của bạn.*
Nó viết lại mọi đối tượng commit kể từ tree sớm nhất bạn phải sửa đổi để xóa một tham chiếu tập tin lớn.
Nếu bạn làm điều này ngay sau khi nhập, trước khi bất kỳ ai bắt đầu dựa công việc trên commit, bạn ổn – nếu không, bạn phải thông báo cho tất cả những người đóng góp rằng họ phải rebase công việc của họ lên các commit mới của bạn.
Để minh họa, bạn sẽ thêm một tập tin lớn vào kho chứa thử nghiệm của mình, xóa nó trong commit tiếp theo, tìm nó, và xóa nó vĩnh viễn khỏi kho chứa.
Đầu tiên, thêm một đối tượng lớn vào lịch sử của bạn:
[source,console]
----
$ curl -L https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'Add git tarball'
[master 7b30847] Add git tarball
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tgz
----
Ối – bạn không muốn thêm một tarball lớn vào dự án của mình.
Tốt hơn là loại bỏ nó:
[source,console]
----
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'Oops - remove large tarball'
[master dadf725] Oops - remove large tarball
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tgz
----
Bây giờ, `gc` cơ sở dữ liệu của bạn và xem bạn đang sử dụng bao nhiêu không gian:
[source,console]
----
$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)
----
Bạn có thể chạy lệnh `count-objects` để nhanh chóng xem bạn đang sử dụng bao nhiêu không gian:
[source,console]
----
$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0
----
Mục `size-pack` là kích thước của các packfile của bạn tính bằng kilobyte, vì vậy bạn đang sử dụng gần 5MB.
Trước commit cuối cùng, bạn đã sử dụng gần 2K – rõ ràng, việc xóa tập tin khỏi commit trước đó không xóa nó khỏi lịch sử của bạn.
Mỗi khi ai đó sao chép kho chứa này, họ sẽ phải sao chép tất cả 5MB chỉ để có được dự án nhỏ này, vì bạn vô tình đã thêm một tập tin lớn.
Hãy loại bỏ nó.
Đầu tiên bạn phải tìm nó.
Trong trường hợp này, bạn đã biết tập tin nào.
Nhưng giả sử bạn không biết; bạn sẽ xác định tập tin hoặc các tập tin nào đang chiếm quá nhiều không gian như thế nào?
Nếu bạn chạy `git gc`, tất cả các đối tượng đều ở trong một packfile; bạn có thể xác định các đối tượng lớn bằng cách chạy một lệnh plumbing khác gọi là `git verify-pack` và sắp xếp trên trường thứ ba trong đầu ra, đó là kích thước tập tin.
Bạn cũng có thể chuyển nó qua lệnh `tail` vì bạn chỉ quan tâm đến một vài tập tin lớn nhất cuối cùng:
[source,console]
----
$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
| sort -k 3 -n \
| tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438
----
Đối tượng lớn ở dưới cùng: 5MB.
Để tìm ra tập tin nào, bạn sẽ sử dụng lệnh `rev-list`, mà bạn đã sử dụng ngắn gọn trong <<ch08-customizing-git#_enforcing_commit_message_format>>.
Nếu bạn truyền `--objects` cho `rev-list`, nó liệt kê tất cả các SHA-1 commit và cũng các SHA-1 blob với các đường dẫn tập tin liên quan đến chúng.
Bạn có thể sử dụng điều này để tìm tên blob của mình:
[source,console]
----
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
----
Bây giờ, bạn cần xóa tập tin này khỏi tất cả các tree trong quá khứ của bạn.
Bạn có thể dễ dàng thấy các commit nào đã sửa đổi tập tin này:
[source,console]
----
$ git log --oneline --branches -- git.tgz
dadf725 Oops - remove large tarball
7b30847 Add git tarball
----
Bạn phải viết lại tất cả các commit xuôi dòng từ `7b30847` để xóa hoàn toàn tập tin này khỏi lịch sử Git của bạn.
Để làm như vậy, bạn sử dụng `filter-branch`, mà bạn đã sử dụng trong <<ch07-git-tools#_rewriting_history>>:
[source,console]
----
$ git filter-branch --index-filter \
'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten
----
Tùy chọn `--index-filter` tương tự như tùy chọn `--tree-filter` được sử dụng trong <<ch07-git-tools#_rewriting_history>>, ngoại trừ thay vì truyền một lệnh sửa đổi các tập tin được check out trên đĩa, bạn đang sửa đổi khu vực tổ chức hoặc index của mình mỗi lần.
Thay vì xóa một tập tin cụ thể bằng một cái gì đó như `rm file`, bạn phải xóa nó bằng `git rm --cached` – bạn phải xóa nó khỏi index, không phải khỏi đĩa.
Lý do để làm theo cách này là tốc độ – vì Git không phải check out mỗi bản sửa đổi ra đĩa trước khi chạy bộ lọc của bạn, quá trình có thể nhanh hơn nhiều, nhiều lắm.
Bạn có thể hoàn thành cùng một nhiệm vụ với `--tree-filter` nếu bạn muốn.
Tùy chọn `--ignore-unmatch` cho `git rm` bảo nó không lỗi ra nếu mẫu bạn đang cố gắng xóa không ở đó.
Cuối cùng, bạn yêu cầu `filter-branch` viết lại lịch sử của bạn chỉ từ commit `7b30847` trở lên, vì bạn biết đó là nơi vấn đề này bắt đầu.
Nếu không, nó sẽ bắt đầu từ đầu và sẽ mất thời gian không cần thiết.
Lịch sử của bạn không còn chứa một tham chiếu đến tập tin đó nữa.
Tuy nhiên, reflog của bạn và một bộ tham chiếu mới mà Git đã thêm khi bạn thực hiện `filter-branch` dưới `.git/refs/original` vẫn còn, vì vậy bạn phải xóa chúng và sau đó đóng gói lại cơ sở dữ liệu.
Bạn cần loại bỏ bất cứ thứ gì có con trỏ đến các commit cũ đó trước khi bạn đóng gói lại:
[source,console]
----
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)
----
Hãy xem bạn đã tiết kiệm được bao nhiêu không gian.
[source,console]
----
$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
----
Kích thước kho chứa được đóng gói giảm xuống còn 8K, tốt hơn nhiều so với 5MB.
Bạn có thể thấy từ giá trị size rằng đối tượng lớn vẫn còn trong các đối tượng loose của bạn, vì vậy nó không biến mất; nhưng nó sẽ không được truyền trên một lần đẩy hoặc bản sao tiếp theo, đó là điều quan trọng.
Nếu bạn thực sự muốn, bạn có thể xóa đối tượng hoàn toàn bằng cách chạy `git prune` với tùy chọn `--expire`:
[source,console]
----
$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0
----
=== Biến Môi trường
Git luôn chạy bên trong một shell `bash`, và sử dụng một số biến môi trường shell để xác định cách nó hoạt động.
Thỉnh thoảng, sẽ hữu ích khi biết những biến này là gì, và cách chúng có thể được sử dụng để làm cho Git hoạt động theo cách bạn muốn.
Đây không phải là danh sách đầy đủ tất cả các biến môi trường mà Git chú ý đến, nhưng chúng tôi sẽ đề cập đến những biến hữu ích nhất.
==== Hành vi Toàn cục
Một số hành vi chung của Git như một chương trình máy tính phụ thuộc vào các biến môi trường.
*`GIT_EXEC_PATH`* xác định nơi Git tìm kiếm các chương trình con của nó (như `git-commit`, `git-diff`, và các chương trình khác).
Bạn có thể kiểm tra thiết lập hiện tại bằng cách chạy `git --exec-path`.
*`HOME`* thường không được coi là có thể tùy chỉnh (quá nhiều thứ khác phụ thuộc vào nó), nhưng đó là nơi Git tìm kiếm tập tin cấu hình toàn cục.
Nếu bạn muốn một cài đặt Git thực sự di động, hoàn chỉnh với cấu hình toàn cục, bạn có thể ghi đè `HOME` trong hồ sơ shell của Git di động.
*`PREFIX`* tương tự, nhưng cho cấu hình toàn hệ thống.
Git tìm kiếm tập tin này tại `$PREFIX/etc/gitconfig`.
*`GIT_CONFIG_NOSYSTEM`*, nếu được đặt, vô hiệu hóa việc sử dụng tập tin cấu hình toàn hệ thống.
Điều này hữu ích nếu cấu hình hệ thống của bạn đang can thiệp vào các lệnh của bạn, nhưng bạn không có quyền truy cập để thay đổi hoặc xóa nó.
*`GIT_PAGER`* kiểm soát chương trình được sử dụng để hiển thị đầu ra nhiều trang trên dòng lệnh.
Nếu điều này không được đặt, `PAGER` sẽ được sử dụng như một dự phòng.
*`GIT_EDITOR`* là trình soạn thảo mà Git sẽ khởi chđộng khi người dùng cần chỉnh sửa một số văn bản (một thông điệp commit, chẳng hạn).
Nếu không được đặt, `EDITOR` sẽ được sử dụng.
==== Vị trí Kho chứa
Git sử dụng một số biến môi trường để xác định cách nó giao tiếp với kho chứa hiện tại.
*`GIT_DIR`* là vị trí của thư mục `.git`.
Nếu điều này không được chỉ định, Git đi lên cây thư mục cho đến khi nó đến `~` hoặc `/`, tìm kiếm một thư mục `.git` ở mỗi bước.
*`GIT_CEILING_DIRECTORIES`* kiểm soát hành vi tìm kiếm một thư mục `.git`.
Nếu bạn truy cập các thư mục chậm để tải (chẳng hạn như những thư mục trên ổ băng, hoặc qua một kết nối mạng chậm), bạn có thể muốn Git ngừng thử sớm hơn so với nó có thể, đặc biệt nếu Git được gọi khi xây dựng dấu nhắc shell của bạn.
*`GIT_WORK_TREE`* là vị trí của gốc của thư mục làm việc cho một kho chứa không bare.
Nếu `--git-dir` hoặc `GIT_DIR` được chỉ định nhưng không có `--work-tree`, `GIT_WORK_TREE` hoặc `core.worktree` được chỉ định, thư mục làm việc hiện tại được coi là cấp cao nhất của cây làm việc của bạn.
*`GIT_INDEX_FILE`* là đường dẫn đến tập tin index (chỉ các kho chứa không bare).
*`GIT_OBJECT_DIRECTORY`* có thể được sử dụng để chỉ định vị trí của thư mục thường nằm tại `.git/objects`.
*`GIT_ALTERNATE_OBJECT_DIRECTORIES`* là một danh sách phân tách bằng dấu hai chấm (được định dạng như `/dir/one:/dir/two:…`) cho Git biết nơi kiểm tra các đối tượng nếu chúng không có trong `GIT_OBJECT_DIRECTORY`.
Nếu bạn tình cờ có nhiều dự án với các tập tin lớn có nội dung giống hệt nhau, điều này có thể được sử dụng để tránh lưu trữ quá nhiều bản sao của chúng.
==== Pathspecs
Một "`pathspec`" đề cập đến cách bạn chỉ định đường dẫn đến các thứ trong Git, bao gồm việc sử dụng ký tự đại diện.
Chúng được sử dụng trong tập tin `.gitignore`, nhưng cũng trên dòng lệnh (`git add *.c`).
*`GIT_GLOB_PATHSPECS`* và *`GIT_NOGLOB_PATHSPECS`* kiểm soát hành vi mặc định của ký tự đại diện trong pathspecs.
Nếu `GIT_GLOB_PATHSPECS` được đặt thành 1, các ký tự đại diện hoạt động như ký tự đại diện (đó là mặc định); nếu `GIT_NOGLOB_PATHSPECS` được đặt thành 1, các ký tự đại diện chỉ khớp với chính chúng, có nghĩa là một cái gì đó như `\\*.c` sẽ chỉ khớp với một tập tin _có tên_ "`\\*.c`", thay vì bất kỳ tập tin nào có tên kết thúc bằng `.c`.
Bạn có thể ghi đè điều này trong các trường hợp riêng lẻ bằng cách bắt đầu pathspec với `:(glob)` hoặc `:(literal)`, như trong `:(glob)\\*.c`.
*`GIT_LITERAL_PATHSPECS`* vô hiệu hóa cả hai hành vi trên; không có ký tự đại diện nào sẽ hoạt động, và các tiền tố ghi đè cũng bị vô hiệu hóa.
*`GIT_ICASE_PATHSPECS`* đặt tất cả pathspecs hoạt động theo cách không phân biệt chữ hoa chữ thường.
==== Commit
Việc tạo cuối cùng của một đối tượng commit Git thường được thực hiện bởi `git-commit-tree`, sử dụng các biến môi trường này làm nguồn thông tin chính của nó, chỉ quay lại các giá trị cấu hình nếu những biến này không có.
*`GIT_AUTHOR_NAME`* là tên có thể đọc được của con người trong trường "`author`".
*`GIT_AUTHOR_EMAIL`* là email cho trường "`author`".
*`GIT_AUTHOR_DATE`* là dấu thời gian được sử dụng cho trường "`author`".
*`GIT_COMMITTER_NAME`* đặt tên con người cho trường "`committer`".
*`GIT_COMMITTER_EMAIL`* là địa chỉ email cho trường "`committer`".
*`GIT_COMMITTER_DATE`* được sử dụng cho dấu thời gian trong trường "`committer`".
*`EMAIL`* là địa chỉ email dự phòng trong trường hợp giá trị cấu hình `user.email` không được đặt.
Nếu _điều này_ không được đặt, Git quay lại tên người dùng và máy chủ hệ thống.
==== Mạng
Git sử dụng thư viện `curl` để thực hiện các thao tác mạng qua HTTP, vì vậy *`GIT_CURL_VERBOSE`* bảo Git phát ra tất cả các thông điệp được tạo bởi thư viện đó.
Điều này tương tự như thực hiện `curl -v` trên dòng lệnh.
*`GIT_SSL_NO_VERIFY`* bảo Git không xác minh chứng chỉ SSL.
Điều này đôi khi có thể cần thiết nếu bạn đang sử dụng một chứng chỉ tự ký để phục vụ các kho chứa Git qua HTTPS, hoặc bạn đang ở giữa việc thiết lập một máy chủ Git nhưng chưa cài đặt một chứng chỉ đầy đủ.
Nếu tốc độ dữ liệu của một thao tác HTTP thấp hơn *`GIT_HTTP_LOW_SPEED_LIMIT`* byte mỗi giây trong thời gian dài hơn *`GIT_HTTP_LOW_SPEED_TIME`* giây, Git sẽ hủy bỏ thao tác đó.
Các giá trị này ghi đè các giá trị cấu hình `http.lowSpeedLimit` và `http.lowSpeedTime`.
*`GIT_HTTP_USER_AGENT`* đặt chuỗi user-agent được sử dụng bởi Git khi giao tiếp qua HTTP.
Mặc định là một giá trị như `git/2.0.0`.
==== Diffing và Merging
*`GIT_DIFF_OPTS`* là một chút sai tên.
Các giá trị hợp lệ duy nhất là `-u<n>` hoặc `--unified=<n>`, kiểm soát số dòng ngữ cảnh được hiển thị trong một lệnh `git diff`.
*`GIT_EXTERNAL_DIFF`* được sử dụng như một ghi đè cho giá trị cấu hình `diff.external`.
Nếu nó được đặt, Git sẽ gọi chương trình này khi `git diff` được gọi.
*`GIT_DIFF_PATH_COUNTER`* và *`GIT_DIFF_PATH_TOTAL`* hữu ích từ bên trong chương trình được chỉ định bởi `GIT_EXTERNAL_DIFF` hoặc `diff.external`.
Cái trước đại diện cho tập tin nào trong một loạt đang được diff (bắt đầu với 1), và cái sau là tổng số tập tin trong lô.
*`GIT_MERGE_VERBOSITY`* kiểm soát đầu ra cho chiến lược trộn đệ quy.
Các giá trị được phép như sau:
* 0 không xuất ra gì, ngoại trừ có thể một thông điệp lỗi duy nhất.
* 1 chỉ hiển thị xung đột.
* 2 cũng hiển thị các thay đổi tập tin.
* 3 hiển thị khi các tập tin bị bỏ qua vì chúng không thay đổi.
* 4 hiển thị tất cả các đường dẫn khi chúng được xử lý.
* 5 trở lên hiển thị thông tin gỡ lỗi chi tiết.
Giá trị mặc định là 2.
==== Gỡ lỗi
Bạn muốn _thực sự_ biết Git đang làm gì?
Git có một bộ dấu vết khá đầy đủ được nhúng, và tất cả những gì bạn cần làm là bật chúng lên.
Các giá trị có thể có của các biến này như sau:
* "`true`", "`1`", hoặc "`2`" – danh mục dấu vết được ghi vào stderr.
* Một đường dẫn tuyệt đối bắt đầu bằng `/` – đầu ra dấu vết sẽ được ghi vào tập tin đó.
*`GIT_TRACE`* kiểm soát các dấu vết chung, không phù hợp với bất kỳ danh mục cụ thể nào.
Điều này bao gồm việc mở rộng các bí danh, và ủy quyền cho các chương trình con khác.
[source,console]
----
$ GIT_TRACE=true git lga
20:12:49.877982 git.c:554 trace: exec: 'git-lga'
20:12:49.878369 run-command.c:341 trace: run_command: 'git-lga'
20:12:49.879529 git.c:282 trace: alias expansion: lga => 'log' '--graph' '--pretty=oneline' '--abbrev-commit' '--decorate' '--all'
20:12:49.879885 git.c:349 trace: built-in: git 'log' '--graph' '--pretty=oneline' '--abbrev-commit' '--decorate' '--all'
20:12:49.899217 run-command.c:341 trace: run_command: 'less'
20:12:49.899675 run-command.c:192 trace: exec: 'less'
----
*`GIT_TRACE_PACK_ACCESS`* kiểm soát dấu vết truy cập packfile.
Trường đầu tiên là packfile đang được truy cập, trường thứ hai là offset trong tập tin đó:
[source,console]
----
$ GIT_TRACE_PACK_ACCESS=true git status
20:10:12.081397 sha1_file.c:2088 .git/objects/pack/pack-c3fa...291e.pack 12
20:10:12.081886 sha1_file.c:2088 .git/objects/pack/pack-c3fa...291e.pack 34662
20:10:12.082115 sha1_file.c:2088 .git/objects/pack/pack-c3fa...291e.pack 35175
# […]
20:10:12.087398 sha1_file.c:2088 .git/objects/pack/pack-e80e...e3d2.pack 56914983
20:10:12.087419 sha1_file.c:2088 .git/objects/pack/pack-e80e...e3d2.pack 14303666
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
----
*`GIT_TRACE_PACKET`* cho phép dấu vết cấp gói cho các thao tác mạng.
[source,console]
----
$ GIT_TRACE_PACKET=true git ls-remote origin
20:15:14.867043 pkt-line.c:46 packet: git< # service=git-upload-pack
20:15:14.867071 pkt-line.c:46 packet: git< 0000
20:15:14.867079 pkt-line.c:46 packet: git< 97b8860c071898d9e162678ea1035a8ced2f8b1f HEAD\\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/master agent=git/2.0.4
20:15:14.867088 pkt-line.c:46 packet: git< 0f20ae29889d61f2e93ae00fd34f1cdb53285702 refs/heads/ab/add-interactive-show-diff-func-name
20:15:14.867094 pkt-line.c:46 packet: git< 36dc827bc9d17f80ed4f326de21247a5d1341fbc refs/heads/ah/doc-gitk-config
# […]
----
*`GIT_TRACE_PERFORMANCE`* kiểm soát ghi nhật ký dữ liệu hiệu suất.
Đầu ra hiển thị mỗi lệnh gọi `git` cụ thể mất bao lâu.
[source,console]
----
$ GIT_TRACE_PERFORMANCE=true git gc
20:18:19.499676 trace.c:414 performance: 0.374835000 s: git command: 'git' 'pack-refs' '--all' '--prune'
20:18:19.845585 trace.c:414 performance: 0.343020000 s: git command: 'git' 'reflog' 'expire' '--all'
Counting objects: 170994, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (43413/43413), done.
Writing objects: 100% (170994/170994), done.
Total 170994 (delta 126176), reused 170524 (delta 125706)
20:18:23.567927 trace.c:414 performance: 3.715349000 s: git command: 'git' 'pack-objects' '--keep-true-parents' '--honor-pack-keep' '--non-empty' '--all' '--reflog' '--unpack-unreachable=2.weeks.ago' '--local' '--delta-base-offset' '.git/objects/pack/.tmp-49190-pack'
20:18:23.584728 trace.c:414 performance: 0.000910000 s: git command: 'git' 'prune-packed'
20:18:23.605218 trace.c:414 performance: 0.017972000 s: git command: 'git' 'update-server-info'
20:18:23.606342 trace.c:414 performance: 3.756312000 s: git command: 'git' 'repack' '-d' '-l' '-A' '--unpack-unreachable=2.weeks.ago'
Checking connectivity: 170994, done.
20:18:25.225424 trace.c:414 performance: 1.616423000 s: git command: 'git' 'prune' '--expire' '2.weeks.ago'
20:18:25.232403 trace.c:414 performance: 0.001051000 s: git command: 'git' 'rerere' 'gc'
20:18:25.233159 trace.c:414 performance: 6.112217000 s: git command: 'git' 'gc'
----
*`GIT_TRACE_SETUP`* hiển thị thông tin về những gì Git đang khám phá về kho chứa và môi trường mà nó đang tương tác.
[source,console]
----
$ GIT_TRACE_SETUP=true git status
20:19:47.086765 trace.c:315 setup: git_dir: .git
20:19:47.087184 trace.c:316 setup: worktree: /Users/ben/src/git
20:19:47.087191 trace.c:317 setup: cwd: /Users/ben/src/git
20:19:47.087194 trace.c:318 setup: prefix: (null)
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
----
==== Khác
*`GIT_SSH`*, nếu được chỉ định, là một chương trình được gọi thay vì `ssh` khi Git cố gắng kết nối đến một máy chủ SSH.
Nó được gọi như `$GIT_SSH [username@]host [-p <port>] <command>`.
Lưu ý rằng đây không phải là cách dễ nhất để tùy chỉnh cách `ssh` được gọi; nó sẽ không hỗ trợ các tham số dòng lệnh bổ sung.
Để hỗ trợ các tham số dòng lệnh bổ sung, bạn có thể sử dụng *`GIT_SSH_COMMAND`*, viết một tập tin kịch bản wrapper và đặt `GIT_SSH` trỏ đến nó hoặc sử dụng tập tin `~/.ssh/config`.
*`GIT_SSH_COMMAND`* đặt lệnh SSH được sử dụng khi Git cố gắng kết nối đến một máy chủ SSH.
Lệnh được thông dịch bởi shell, và các đối số dòng lệnh bổ sung có thể được sử dụng với `ssh`, chẳng hạn như `GIT_SSH_COMMAND="ssh -i ~/.ssh/my_key" git clone git@example.com:my/repo`.
*`GIT_ASKPASS`* là một ghi đè cho giá trị cấu hình `core.askpass`.
Đây là chương trình được gọi bất cứ khi nào Git cần yêu cầu người dùng nhập thông tin xác thực, có thể mong đợi một dấu nhắc văn bản như một đối số dòng lệnh, và nên trả về câu trả lời trên `stdout` (xem <<ch07-git-tools#_credential_caching>> để biết thêm về hệ thống con này).
*`GIT_NAMESPACE`* kiểm soát quyền truy cập vào các tham chiếu có namespace, và tương đương với cờ `--namespace`.
Điều này chủ yếu hữu ích ở phía máy chủ, nơi bạn có thể muốn lưu trữ nhiều fork của một kho chứa duy nhất trong một kho chứa, chỉ giữ các tham chiếu riêng biệt.
*`GIT_FLUSH`* có thể được sử dụng để buộc Git sử dụng I/O không được đệm khi ghi dần dần vào stdout.
Giá trị 1 khiến Git xả thường xuyên hơn, giá trị 0 khiến tất cả đầu ra được đệm.
Giá trị mặc định (nếu biến này không được đặt) là chọn một lược đồ đệm thích hợp tùy thuộc vào hoạt động và chế độ đầu ra.
*`GIT_REFLOG_ACTION`* cho phép bạn chỉ định văn bản mô tả được ghi vào reflog.
Đây là một ví dụ:
[source,console]
----
$ GIT_REFLOG_ACTION="my action" git commit --allow-empty -m 'My message'
[master 9e3d55a] My message
$ git reflog -1
9e3d55a HEAD@{0}: my action: My message
----
=== Summary
At this point, you should have a pretty good understanding of what Git does in the background and, to some degree, how it's implemented.
This chapter has covered a number of plumbing commands -- commands that are lower level and simpler than the porcelain commands you've learned about in the rest of the book.
Understanding how Git works at a lower level should make it easier to understand why it's doing what it's doing and also to write your own tools and helper scripts to make your specific workflow work for you.
Git như một hệ thống tập tin định vị theo nội dung là một công cụ rất mạnh mà bạn có thể tận dụng nhiều hơn chỉ là một VCS.
Hy vọng bạn sẽ dùng kiến thức mới về nội bộ Git để triển khai các ứng dụng thú vị và cảm thấy thoải mái hơn khi dùng Git ở các mức nâng cao.
[[A-git-in-other-environments]]
[appendix]
== Git trong các môi trường khác
Nếu bạn đã đọc hết cả cuốn sách này, bạn đã học được rất nhiều về cách sử dụng Git trên dòng lệnh.
Bạn có thể làm việc với các tệp cục bộ, kết nối kho lưu trữ của mình với những người khác qua mạng và làm việc hiệu quả với những người khác.
Nhưng câu chuyện không kết thúc ở đó; Git thường được sử dụng như một phần của một hệ sinh thái lớn hơn và không phải lúc nào dòng lệnh cũng là cách tốt nhất để làm việc với nó.
Bây giờ chúng ta sẽ xem xét một số loại môi trường khác mà Git có thể hữu ích và cách các ứng dụng khác (bao gồm cả ứng dụng của bạn) hoạt động cùng với Git.
=== Giao diện Đồ họa
(((GUIs)))(((Graphical tools)))
Môi trường tự nhiên của Git là trong terminal.
Các tính năng mới xuất hiện ở đó trước tiên, và chỉ ở dòng lệnh là sức mạnh đầy đủ của Git hoàn toàn có sẵn cho bạn.
Nhưng văn bản thuần túy không phải là lựa chọn tốt nhất cho tất cả các nhiệm vụ; đôi khi một biểu diễn trực quan là những gì bạn cần, và một số người dùng thoải mái hơn nhiều với giao diện trỏ và nhấp.
Điều quan trọng cần lưu ý là các giao diện khác nhau được thiết kế cho các quy trình làm việc khác nhau.
Một số máy khách chỉ hiển thị một tập hợp con được tuyển chọn cẩn thận của chức năng Git, để hỗ trợ một cách làm việc cụ thể mà tác giả cho là hiệu quả.
Khi được xem dưới góc độ này, không có công cụ nào trong số này có thể được gọi là "`tốt hơn`" bất kỳ công cụ nào khác, chúng chỉ đơn giản là phù hợp hơn cho mục đích dự định của chúng.
Cũng lưu ý rằng không có gì các máy khách đồ họa này có thể làm mà máy khách dòng lệnh không thể; dòng lệnh vẫn là nơi bạn sẽ có nhiều quyền lực và kiểm soát nhất khi làm việc với các kho chứa của mình.
==== `gitk` và `git-gui`
(((git commands, gitk)))(((git commands, gui)))(((gitk)))
Khi bạn cài đặt Git, bạn cũng nhận được các công cụ trực quan của nó, `gitk` và `git-gui`.
`gitk` là một trình xem lịch sử đồ họa.
Hãy nghĩ về nó như một shell GUI mạnh mẽ trên `git log` và `git grep`.
Đây là công cụ để sử dụng khi bạn đang cố gắng tìm một cái gì đó đã xảy ra trong quá khứ, hoặc trực quan hóa lịch sử dự án của bạn.
Gitk dễ nhất để gọi từ dòng lệnh.
Chỉ cần `cd` vào một kho chứa Git, và gõ:
[source,console]
----
$ gitk [git log options]
----
Gitk chấp nhận nhiều tùy chọn dòng lệnh, hầu hết trong số đó được truyền qua hành động `git log` bên dưới.
Có lẽ một trong những hữu ích nhất là cờ `--all`, bảo `gitk` hiển thị các commit có thể truy cập được từ _bất kỳ_ ref nào, không chỉ HEAD.
Giao diện của Gitk trông như thế này:
.Trình xem lịch sử `gitk`
image::images/gitk.png[Trình xem lịch sử `gitk`]
Ở trên cùng là một cái gì đó trông hơi giống như đầu ra của `git log --graph`; mỗi dấu chấm đại diện cho một commit, các dòng đại diện cho mối quan hệ cha mẹ, và các ref được hiển thị dưới dạng các hộp màu.
Dấu chấm màu vàng đại diện cho HEAD, và dấu chấm màu đỏ đại diện cho các thay đổi chưa trở thành một commit.
Ở dưới cùng là một view của commit đã chọn; các comment và patch ở bên trái, và một view tóm tắt ở bên phải.
Ở giữa là một tập hợp các điều khiển được sử dụng để tìm kiếm lịch sử.
Mặt khác, `git-gui` chủ yếu là một công cụ để tạo các commit.
Nó cũng dễ nhất để gọi từ dòng lệnh:
[source,console]
----
$ git gui
----
Và nó trông giống như thế này:
.Công cụ commit `git-gui`
image::images/git-gui.png[Công cụ commit `git-gui`]
Ở bên trái là index; các thay đổi chưa được staged ở trên cùng, các thay đổi đã staged ở dưới cùng.
Bạn có thể di chuyển toàn bộ tập tin giữa hai trạng thái bằng cách nhấp vào biểu tượng của chúng, hoặc bạn có thể chọn một tập tin để xem bằng cách nhấp vào tên của nó.
Ở trên cùng bên phải là view diff, hiển thị các thay đổi cho tập tin hiện đang được chọn.
Bạn có thể stage các hunk riêng lẻ (hoặc các dòng riêng lẻ) bằng cách nhấp chuột phải trong khu vực này.
Ở dưới cùng bên phải là khu vực thông điệp và hành động.
Gõ thông điệp của bạn vào hộp văn bản và nhấp "`Commit`" để làm một cái gì đó tương tự như `git commit`.
Bạn cũng có thể chọn sửa đổi commit cuối cùng bằng cách chọn nút radio "`Amend`", sẽ cập nhật khu vực "`Staged Changes`" với nội dung của commit cuối cùng.
Sau đó bạn chỉ cần stage hoặc unstage một số thay đổi, thay đổi thông điệp commit, và nhấp "`Commit`" lại để thay thế commit cũ bằng một commit mới.
`gitk` và `git-gui` là ví dụ về các công cụ hướng nhiệm vụ.
Mỗi cái trong số chúng được thiết kế cho một mục đích cụ thể (xem lịch sử và tạo commit, tương ứng), và bỏ qua các tính năng không cần thiết cho nhiệm vụ đó.
==== GitHub cho macOS và Windows
(((GitHub for macOS)))(((GitHub for Windows)))
GitHub đã tạo hai máy khách Git hướng quy trình làm việc: một cho Windows, và một cho macOS.
Các máy khách này là một ví dụ tốt về các công cụ hướng quy trình làm việc – thay vì hiển thị _tất cả_ chức năng của Git, chúng thay vào đó tập trung vào một tập hợp các tính năng thường được sử dụng được tuyển chọn hoạt động tốt cùng nhau.
Chúng trông như thế này:
.GitHub cho macOS
image::images/github_mac.png[GitHub cho macOS]
.GitHub cho Windows
image::images/github_win.png[GitHub cho Windows]
Chúng được thiết kế để trông và hoạt động rất giống nhau, vì vậy chúng tôi sẽ coi chúng như một sản phẩm duy nhất trong chương này.
Chúng tôi sẽ không thực hiện một đánh giá chi tiết về các công cụ này (chúng có tài liệu riêng của chúng), nhưng một tour nhanh về view "`changes`" (nơi bạn sẽ dành phần lớn thời gian của mình) là cần thiết.
* Ở bên trái là danh sách các kho chứa mà máy khách đang theo dõi; bạn có thể thêm một kho chứa (bằng cách sao chép hoặc đính kèm cục bộ) bằng cách nhấp vào biểu tượng "`+`" ở đầu khu vực này.
* Ở trung tâm là một khu vực nhập commit, cho phép bạn nhập thông điệp commit, và chọn tập tin nào nên được bao gồm.
Trên Windows, lịch sử commit được hiển thị trực tiếp bên dưới đây; trên macOS, nó ở trên một tab riêng biệt.
* Ở bên phải là một view diff, hiển thị những gì đã thay đổi trong thư mục làm việc của bạn, hoặc những thay đổi nào đã được bao gồm trong commit đã chọn.
* Điều cuối cùng cần chú ý là nút "`Sync`" ở trên cùng bên phải, đó là cách chính bạn tương tác qua mạng.
[NOTE]
====
Bạn không cần tài khoản GitHub để sử dụng các công cụ này.
Mặc dù chúng được thiết kế để làm nổi bật dịch vụ và quy trình làm việc được khuyến nghị của GitHub, chúng sẽ vui vẻ làm việc với bất kỳ kho chứa nào, và thực hiện các thao tác mạng với bất kỳ máy chủ Git nào.
====
===== Cài đặt
GitHub cho Windows và macOS có thể được tải xuống từ https://desktop.github.com/[^].
Khi các ứng dụng được chạy lần đầu tiên, chúng hướng dẫn bạn qua tất cả các thiết lập Git lần đầu tiên, chẳng hạn như cấu hình tên và địa chỉ email của bạn, và cả hai đều thiết lập các mặc định hợp lý cho nhiều tùy chọn cấu hình phổ biến, chẳng hạn như bộ nhớ cache thông tin xác thực và hành vi CRLF.
Cả hai đều là "`evergreen`" – các bản cập nhật được tải xuống và cài đặt ở chế độ nền trong khi các ứng dụng đang mở.
Điều này hữu ích bao gồm một phiên bản Git được đóng gói, có nghĩa là bạn có lẽ sẽ không phải lo lắng về việc cập nhật thủ công nó nữa.
Trên Windows, máy khách bao gồm một lối tắt để khởi chạy PowerShell với Posh-git, mà chúng ta sẽ nói thêm về sau trong chương này.
Bước tiếp theo là cung cấp cho công cụ một số kho chứa để làm việc.
Máy khách hiển thị cho bạn một danh sách các kho chứa mà bạn có quyền truy cập trên GitHub, và có thể sao chép chúng trong một bước.
Nếu bạn đã có một kho chứa cục bộ, chỉ cần kéo thư mục của nó từ Finder hoặc Windows Explorer vào cửa sổ máy khách GitHub, và nó sẽ được bao gồm trong danh sách các kho chứa ở bên trái.
===== Quy trình Làm việc Được khuyến nghị
Khi đã được cài đặt và cấu hình, bạn có thể sử dụng máy khách GitHub cho nhiều nhiệm vụ Git phổ biến.
Quy trình làm việc dự định cho công cụ này đôi khi được gọi là "`GitHub Flow`".
Chúng tôi đề cập điều này chi tiết hơn trong <<ch06-github#ch06-github_flow>>, nhưng ý chính chung là (a) bạn sẽ commit vào một nhánh, và (b) bạn sẽ đồng bộ hóa với một kho chứa từ xa khá thường xuyên.
Quản lý nhánh là một trong những khu vực mà hai công cụ khác nhau.
Trên macOS, có một nút ở đầu cửa sổ để tạo một nhánh mới:
.Nút "`Create Branch`" trên macOS
image::images/branch_widget_mac.png[Nút "Create Branch" trên macOS]
Trên Windows, điều này được thực hiện bằng cách gõ tên nhánh mới trong widget chuyển đổi nhánh:
.Tạo một nhánh trên Windows
image::images/branch_widget_win.png[Tạo một nhánh trên Windows]
Khi nhánh của bạn được tạo, việc tạo các commit mới khá đơn giản.
Thực hiện một số thay đổi trong thư mục làm việc của bạn, và khi bạn chuyển sang cửa sổ máy khách GitHub, nó sẽ hiển thị cho bạn tập tin nào đã thay đổi.
Nhập một thông điệp commit, chọn các tập tin bạn muốn bao gồm, và nhấp nút "`Commit`" (ctrl-enter hoặc ⌘-enter).
Cách chính bạn tương tác với các kho chứa khác qua mạng là thông qua tính năng "`Sync`".
Git nội bộ có các thao tác riêng biệt cho pushing, fetching, merging, và rebasing, nhưng các máy khách GitHub thu gọn tất cả những thứ này thành một tính năng nhiều bước.
Đây là những gì xảy ra khi bạn nhấp nút Sync:
. `git pull --rebase`.
Nếu điều này thất bại vì xung đột merge, quay lại `git pull --no-rebase`.
. `git push`.
Đây là chuỗi phổ biến nhất của các lệnh mạng khi làm việc theo kiểu này, vì vậy việc nén chúng thành một lệnh tiết kiệm rất nhiều thời gian.
===== Tóm tắt
Các công cụ này rất phù hợp với quy trình làm việc mà chúng được thiết kế.
Các nhà phát triển và không phải nhà phát triển có thể cộng tác trên một dự án trong vòng vài phút, và nhiều thực hành tốt nhất cho loại quy trình làm việc này được tích hợp vào các công cụ.
Tuy nhiên, nếu quy trình làm việc của bạn khác, hoặc bạn muốn kiểm soát nhiều hơn về cách thức và thời điểm các thao tác mạng được thực hiện, chúng tôi khuyên bạn sử dụng một máy khách khác hoặc dòng lệnh.
==== Các GUI Khác
Có một số máy khách Git đồ họa khác, và chúng chạy từ các công cụ chuyên biệt, một mục đích duy nhất đến các ứng dụng cố gắng hiển thị mọi thứ Git có thể làm.
Trang web Git chính thức có một danh sách được tuyển chọn của các máy khách phổ biến nhất tại https://git-scm.com/downloads/guis[^].
Một danh sách toàn diện hơn có sẵn trên trang wiki Git, tại https://archive.kernel.org/oldwiki/git.wiki.kernel.org/index.php/Interfaces,_frontends,_and_tools.html#Graphical_Interfaces[^].
=== Git trong Visual Studio
(((Visual Studio)))
Visual Studio có công cụ Git được tích hợp trực tiếp vào IDE, bắt đầu từ Visual Studio 2019 phiên bản 16.8.
Công cụ hỗ trợ các chức năng Git sau:
* Tạo hoặc sao chép một kho chứa.
* Mở và duyệt lịch sử của một kho chứa.
* Tạo và checkout các nhánh và tag.
* Stash, stage, và commit các thay đổi.
* Fetch, pull, push, hoặc sync các commit.
* Merge và rebase các nhánh.
* Giải quyết xung đột merge.
* Xem diff.
* ... và nhiều hơn nữa!
Đọc https://learn.microsoft.com/en-us/visualstudio/version-control/[tài liệu chính thức^] để tìm hiểu thêm.
=== Git trong Visual Studio Code
(((Visual Studio Code)))
Visual Studio Code có hỗ trợ Git tích hợp sẵn.
Bạn sẽ cần cài đặt Git phiên bản 2.0.0 (hoặc mới hơn).
Các tính năng chính là:
* Xem diff của tập tin bạn đang chỉnh sửa trong gutter.
* Thanh Trạng thái Git (góc dưới bên trái) hiển thị nhánh hiện tại, chỉ báo dirty, các commit đến và đi.
* Bạn có thể thực hiện hầu hết các thao tác git phổ biến từ trong trình soạn thảo:
** Khởi tạo một kho chứa.
** Sao chép một kho chứa.
** Tạo các nhánh và tag.
** Stage và commit các thay đổi.
** Push/pull/sync với một nhánh từ xa.
** Giải quyết xung đột merge.
** Xem diff.
* Với một extension, bạn cũng có thể xử lý GitHub Pull Requests:
https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github[^].
Tài liệu chính thức có thể được tìm thấy tại đây: https://code.visualstudio.com/docs/sourcecontrol/overview[^].
=== Git trong IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine
(((JetBrains)))
Các IDE của JetBrains (như IntelliJ IDEA, PyCharm, WebStorm, PhpStorm, RubyMine, và các IDE khác) đi kèm với plugin Git Integration.
Nó cung cấp một view chuyên dụng trong IDE để làm việc với Git và GitHub Pull Requests.
.Cửa sổ công cụ Kiểm soát Phiên bản trong các IDE JetBrains
image::images/jb.png[Cửa sổ công cụ Kiểm soát Phiên bản trong các IDE JetBrains]
Tích hợp này dựa vào máy khách Git dòng lệnh, và yêu cầu một cái được cài đặt.
Tài liệu chính thức có sẵn tại https://www.jetbrains.com/help/idea/using-git-integration.html[^].
=== Git trong Sublime Text
(((Sublime Text)))
Từ phiên bản 3.2 trở đi, Sublime Text có tích hợp Git trong trình soạn thảo.
Các tính năng là:
* Thanh bên sẽ hiển thị `git status` của các tập tin và thư mục với một huy hiệu/biểu tượng.
* Các tập tin và thư mục nằm trong tập tin `.gitignore` của bạn sẽ bị mờ đi trong thanh bên.
* Trong thanh trạng thái, bạn có thể thấy nhánh Git hiện tại và bạn đã thực hiện bao nhiêu sửa đổi.
* Tất cả các thay đổi đối với một tập tin hiện có thể nhìn thấy thông qua các dấu trong gutter.
* Bạn có thể sử dụng một phần chức năng của máy khách Git Sublime Merge từ trong Sublime Text.
Điều này yêu cầu Sublime Merge được cài đặt.
Xem: https://www.sublimemerge.com/[^].
Tài liệu chính thức cho Sublime Text có thể được tìm thấy tại đây: https://www.sublimetext.com/docs/git_integration.html[^].
=== Git trong Bash
(((bash)))(((tab completion, bash)))(((shell prompts, bash)))
Nếu bạn là người dùng Bash, bạn có thể tận dụng một số tính năng của shell để làm cho trải nghiệm với Git thân thiện hơn nhiều.
Git thực sự đi kèm với các plugin cho một số shell, nhưng nó không được bật theo mặc định.
Đầu tiên, bạn cần lấy một bản sao của tập tin completions từ mã nguồn của phiên bản Git mà bạn đang sử dụng.
Kiểm tra phiên bản của bạn bằng cách gõ `git version`, sau đó sử dụng `git checkout tags/vX.Y.Z`, trong đó `vX.Y.Z` tương ứng với phiên bản Git bạn đang sử dụng.
Sao chép tập tin `contrib/completion/git-completion.bash` đến một nơi tiện lợi, như thư mục home của bạn, và thêm điều này vào `.bashrc` của bạn:
[source,console]
----
. ~/git-completion.bash
----
Khi đã hoàn tất, thay đổi thư mục của bạn sang một kho chứa Git, và gõ:
[source,console]
----
$ git chec<tab>
----
…và Bash sẽ tự động hoàn thành thành `git checkout`.
Điều này hoạt động với tất cả các lệnh con của Git, các tham số dòng lệnh, và tên remote và ref khi thích hợp.
Cũng hữu ích khi tùy chỉnh dấu nhắc của bạn để hiển thị thông tin về kho chứa Git của thư mục hiện tại.
Điều này có thể đơn giản hoặc phức tạp tùy theo bạn muốn, nhưng thường có một vài thông tin quan trọng mà hầu hết mọi người muốn, như nhánh hiện tại, và trạng thái của thư mục làm việc.
Để thêm những thứ này vào dấu nhắc của bạn, chỉ cần sao chép tập tin `contrib/completion/git-prompt.sh` từ kho chứa mã nguồn Git vào thư mục home của bạn, thêm một cái gì đó như thế này vào `.bashrc` của bạn:
[source,console]
----
. ~/git-prompt.sh
export GIT_PS1_SHOWDIRTYSTATE=1
export PS1='\\w$(__git_ps1 \" (%s)\")\\$ '
----
`\\w` có nghĩa là in thư mục làm việc hiện tại, `\\$` in phần `$` của dấu nhắc, và `__git_ps1 \" (%s)\"` gọi hàm được cung cấp bởi `git-prompt.sh` với một đối số định dạng.
Bây giờ dấu nhắc bash của bạn sẽ trông như thế này khi bạn ở bất cứ đâu bên trong một dự án được kiểm soát bởi Git:
.Dấu nhắc `bash` tùy chỉnh
image::images/git-bash.png[Dấu nhắc `bash` tùy chỉnh]
Cả hai tập tin kịch bản này đều đi kèm với tài liệu hữu ích; hãy xem nội dung của `git-completion.bash` và `git-prompt.sh` để biết thêm thông tin.
=== Git trong Zsh
(((zsh)))(((tab completion, zsh)))(((shell prompts, zsh)))
Zsh cũng đi kèm với một thư viện tab-completion cho Git.
Để sử dụng nó, chỉ cần chạy `autoload -Uz compinit && compinit` trong `.zshrc` của bạn.
Giao diện của Zsh mạnh mẽ hơn một chút so với Bash:
[source,console]
----
$ git che<tab>
check-attr -- display gitattributes information
check-ref-format -- ensure that a reference name is well formed
checkout -- checkout branch or paths to working tree
checkout-index -- copy files from index to working directory
cherry -- find commits not merged upstream
cherry-pick -- apply changes introduced by some existing commits
----
Các tab-completion không rõ ràng không chỉ được liệt kê; chúng có các mô tả hữu ích, và bạn có thể điều hướng danh sách bằng đồ họa bằng cách nhấn tab lặp đi lặp lại.
Điều này hoạt động với các lệnh Git, các đối số của chúng, và tên của các thứ bên trong kho chứa (như refs và remotes), cũng như tên tập tin và tất cả những thứ khác mà Zsh biết cách tab-complete.
Zsh đi kèm với một framework để lấy thông tin từ các hệ thống kiểm soát phiên bản, gọi là `vcs_info`.
Để bao gồm tên nhánh trong dấu nhắc ở phía bên phải, hãy thêm các dòng này vào tập tin `~/.zshrc` của bạn:
[source,console]
----
autoload -Uz vcs_info
precmd_vcs_info() { vcs_info }
precmd_functions+=( precmd_vcs_info )
setopt prompt_subst
RPROMPT='${vcs_info_msg_0_}'
# PROMPT='${vcs_info_msg_0_}%# '
zstyle ':vcs_info:git:*' formats '%b'
----
Điều này dẫn đến việc hiển thị nhánh hiện tại ở phía bên phải của cửa sổ terminal, bất cứ khi nào shell của bạn ở bên trong một kho chứa Git.
Phía bên trái cũng được hỗ trợ, tất nhiên; chỉ cần bỏ comment gán cho `PROMPT`.
Nó trông hơi giống như thế này:
.Dấu nhắc `zsh` tùy chỉnh
image::images/zsh-prompt.png[Dấu nhắc `zsh` tùy chỉnh]
Để biết thêm thông tin về `vcs_info`, hãy kiểm tra tài liệu của nó trong trang hướng dẫn `zshcontrib(1)`, hoặc trực tuyến tại https://zsh.sourceforge.io/Doc/Release/User-Contributions.html#Version-Control-Information[^].
Thay vì `vcs_info`, bạn có thể thích tập tin kịch bản tùy chỉnh dấu nhắc đi kèm với Git, gọi là `git-prompt.sh`; xem https://github.com/git/git/blob/master/contrib/completion/git-prompt.sh[^] để biết chi tiết.
`git-prompt.sh` tương thích với cả Bash và Zsh.
Zsh đủ mạnh mẽ để có toàn bộ các framework dành riêng để làm cho nó tốt hơn.
Một trong số đó được gọi là "oh-my-zsh", và nó có thể được tìm thấy tại https://github.com/ohmyzsh/ohmyzsh[^].
Hệ thống plugin của oh-my-zsh đi kèm với tab-completion Git mạnh mẽ, và nó có nhiều "theme" dấu nhắc, nhiều trong số đó hiển thị dữ liệu kiểm soát phiên bản.
<<oh_my_zsh_git>> chỉ là một ví dụ về những gì có thể được thực hiện với hệ thống này.
[[oh_my_zsh_git]]
.Một ví dụ về theme oh-my-zsh
image::images/zsh-oh-my.png[Một ví dụ về theme oh-my-zsh]
[[_git_powershell]]
=== Git trong PowerShell
(((PowerShell)))(((tab completion, PowerShell)))(((shell prompts, PowerShell)))
(((posh-git)))
Terminal dòng lệnh cũ trên Windows (`cmd.exe`) không thực sự có khả năng tùy chỉnh trải nghiệm Git, nhưng nếu bạn đang sử dụng PowerShell, bạn may mắn.
Điều này cũng hoạt động nếu bạn đang chạy PowerShell Core trên Linux hoặc macOS.
Một gói có tên posh-git (https://github.com/dahlbyk/posh-git[^]) cung cấp các tiện ích tab-completion mạnh mẽ, cũng như một dấu nhắc nâng cao để giúp bạn theo dõi trạng thái kho chứa của mình.
Nó trông như thế này:
.PowerShell với Posh-git
image::images/posh-git.png[PowerShell với Posh-git]
==== Cài đặt
===== Điều kiện tiên quyết (chỉ Windows)
Trước khi bạn có thể chạy các tập tin kịch bản PowerShell trên máy của mình, bạn cần đặt `ExecutionPolicy` cục bộ của mình thành `RemoteSigned` (về cơ bản, bất cứ thứ gì ngoại trừ `Undefined` và `Restricted`).
Nếu bạn chọn `AllSigned` thay vì `RemoteSigned`, cả các tập tin kịch bản cục bộ (của riêng bạn) cũng cần được ký số để được thực thi.
Với `RemoteSigned`, chỉ các tập tin kịch bản có `ZoneIdentifier` được đặt thành `Internet` (đã được tải xuống từ web) cần được ký, những cái khác thì không.
Nếu bạn là quản trị viên và muốn đặt nó cho tất cả người dùng trên máy đó, hãy sử dụng `-Scope LocalMachine`.
Nếu bạn là người dùng bình thường, không có quyền quản trị, bạn có thể sử dụng `-Scope CurrentUser` để đặt nó chỉ cho bạn.
Thêm về PowerShell Scopes: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes[^].
Thêm về PowerShell ExecutionPolicy: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy[^].
Để đặt giá trị của `ExecutionPolicy` thành `RemoteSigned` cho tất cả người dùng, hãy sử dụng lệnh tiếp theo:
[source,powershell]
----
> Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy RemoteSigned -Force
----
===== PowerShell Gallery
Nếu bạn có ít nhất PowerShell 5 hoặc PowerShell 4 với PackageManagement được cài đặt, bạn có thể sử dụng trình quản lý gói để cài đặt posh-git cho bạn.
Thêm thông tin về PowerShell Gallery: https://learn.microsoft.com/en-us/powershell/scripting/gallery/overview[^].
[source,powershell]
----
> Install-Module posh-git -Scope CurrentUser -Force
> Install-Module posh-git -Scope CurrentUser -AllowPrerelease -Force # Phiên bản beta mới hơn với hỗ trợ PowerShell Core
----
Nếu bạn muốn cài đặt posh-git cho tất cả người dùng, hãy sử dụng `-Scope AllUsers` thay thế và thực thi lệnh từ một console PowerShell nâng cao.
Nếu lệnh thứ hai thất bại với lỗi như `Module 'PowerShellGet' was not installed by using Install-Module`, bạn sẽ cần chạy một lệnh khác trước:
[source,powershell]
----
> Install-Module PowerShellGet -Force -SkipPublisherCheck
----
Sau đó bạn có thể quay lại và thử lại.
Điều này xảy ra, vì các module đi kèm với Windows PowerShell được ký bằng một chứng chỉ xuất bản khác.
===== Cập nhật Dấu nhắc PowerShell
Để bao gồm thông tin Git trong dấu nhắc của bạn, module posh-git cần được nhập.
Để posh-git được nhập mỗi khi PowerShell khởi động, hãy thực thi lệnh `Add-PoshGitToProfile` sẽ thêm câu lệnh nhập vào tập tin kịch bản `$profile` của bạn.
Tập tin kịch bản này được thực thi mỗi khi bạn mở một console PowerShell mới.
Hãy nhớ rằng, có nhiều tập tin kịch bản `$profile`.
Ví dụ, một cho console và một riêng cho ISE.
[source,powershell]
----
> Import-Module posh-git
> Add-PoshGitToProfile -AllHosts
----
===== Từ Mã nguồn
Chỉ cần tải xuống một bản phát hành posh-git từ https://github.com/dahlbyk/posh-git/releases[^], và giải nén nó.
Sau đó nhập module bằng cách sử dụng đường dẫn đầy đủ đến tập tin `posh-git.psd1`:
[source,powershell]
----
> Import-Module <path-to-uncompress-folder>\\src\\posh-git.psd1
> Add-PoshGitToProfile -AllHosts
----
Điều này sẽ thêm dòng thích hợp vào tập tin `profile.ps1` của bạn, và posh-git sẽ hoạt động lần tiếp theo bạn mở PowerShell.
Để biết mô tả về thông tin tóm tắt trạng thái Git được hiển thị trong dấu nhắc, xem: https://github.com/dahlbyk/posh-git/blob/master/README.md#git-status-summary-information[^]
Để biết thêm chi tiết về cách tùy chỉnh dấu nhắc posh-git của bạn, xem: https://github.com/dahlbyk/posh-git/blob/master/README.md#customization-variables[^].
=== Tóm tắt
Bạn đã học cách khai thác sức mạnh của Git từ bên trong các công cụ bạn sử dụng trong công việc hàng ngày của mình và cả cách truy cập các kho lưu trữ Git từ các chương trình của riêng bạn.
[[B-embedding-git-in-your-applications]]
[appendix]
== Nhúng Git vào ứng dụng của bạn
Nếu ứng dụng của bạn dành cho các nhà phát triển, rất có thể nó sẽ được hưởng lợi từ việc tích hợp với kiểm soát mã nguồn.
Ngay cả các ứng dụng không dành cho nhà phát triển, chẳng hạn như trình soạn thảo tài liệu, cũng có khả năng được hưởng lợi từ các tính năng kiểm soát phiên bản và mô hình của Git hoạt động rất tốt cho nhiều tình huống khác nhau.
Nếu bạn cần tích hợp Git với ứng dụng của mình, về cơ bản bạn có hai lựa chọn: tạo một trình bao (shell) và gọi chương trình dòng lệnh `git` hoặc nhúng thư viện Git vào ứng dụng của bạn.
Ở đây, chúng ta sẽ đề cập đến tích hợp dòng lệnh và một số thư viện Git có thể nhúng phổ biến nhất.
=== Git Dòng lệnh
Một tùy chọn là tạo một tiến trình shell và sử dụng công cụ dòng lệnh Git để thực hiện công việc.
Điều này có lợi ích là chính thống, và tất cả các tính năng của Git đều được hỗ trợ.
Điều này cũng tình cờ khá dễ dàng, vì hầu hết các môi trường thời gian chạy đều có một tiện ích tương đối đơn giản để gọi một tiến trình với các đối số dòng lệnh.
Tuy nhiên, cách tiếp cận này có một số nhược điểm.
Một là tất cả đầu ra đều ở dạng văn bản thuần túy.
Điều này có nghĩa là bạn sẽ phải phân tích định dạng đầu ra thỉnh thoảng thay đổi của Git để đọc thông tin tiến trình và kết quả, điều này có thể không hiệu quả và dễ xảy ra lỗi.
Một cái khác là thiếu khả năng phục hồi lỗi.
Nếu một kho chứa bị hỏng bằng cách nào đó, hoặc người dùng có một giá trị cấu hình sai định dạng, Git sẽ đơn giản từ chối thực hiện nhiều thao tác.
Một cái khác nữa là quản lý tiến trình.
Git yêu cầu bạn duy trì một môi trường shell trên một tiến trình riêng biệt, điều này có thể thêm độ phức tạp không mong muốn.
Cố gắng phối hợp nhiều tiến trình này (đặc biệt là khi có khả năng truy cập cùng một kho chứa từ nhiều tiến trình) có thể là một thách thức khá lớn.
=== Libgit2
(((libgit2)))(((C)))
Một tùy chọn khác có sẵn cho bạn là sử dụng Libgit2.
Libgit2 là một triển khai Git không phụ thuộc, tập trung vào việc có một API tốt đẹp để sử dụng trong các chương trình khác.
Bạn có thể tìm thấy nó tại https://libgit2.org[^].
Đầu tiên, hãy xem API C trông như thế nào.
Đây là một tour nhanh:
[source,c]
----
// Open a repository
git_repository *repo;
int error = git_repository_open(&repo, "/path/to/repository");
// Dereference HEAD to a commit
git_object *head_commit;
error = git_revparse_single(&head_commit, repo, "HEAD^{commit}");
git_commit *commit = (git_commit*)head_commit;
// Print some of the commit's properties
printf("%s", git_commit_message(commit));
const git_signature *author = git_commit_author(commit);
printf("%s <%s>\\n", author->name, author->email);
const git_oid *tree_id = git_commit_tree_id(commit);
// Cleanup
git_commit_free(commit);
git_repository_free(repo);
----
Vài dòng đầu tiên mở một kho chứa Git.
Loại `git_repository` đại diện cho một handle đến một kho chứa với một bộ nhớ cache trong bộ nhớ.
Đây là phương pháp đơn giản nhất, khi bạn biết đường dẫn chính xác đến thư mục làm việc hoặc thư mục `.git` của kho chứa.
Cũng có `git_repository_open_ext` bao gồm các tùy chọn để tìm kiếm, `git_clone` và các hàm bạn để tạo một bản sao cục bộ của một kho chứa từ xa, và `git_repository_init` để tạo một kho chứa hoàn toàn mới.
Khối mã thứ hai sử dụng cú pháp rev-parse (xem <<ch07-git-tools#_branch_references>> để biết thêm về điều này) để lấy commit mà HEAD cuối cùng trỏ đến.
Loại được trả về là một con trỏ `git_object`, đại diện cho một cái gì đó tồn tại trong cơ sở dữ liệu đối tượng Git cho một kho chứa.
`git_object` thực sự là một loại "`cha`" cho một số loại đối tượng khác nhau; bố cục bộ nhớ cho mỗi loại "`con`" giống như cho `git_object`, vì vậy bạn có thể ép kiểu an toàn sang loại đúng.
Trong trường hợp này, `git_object_type(commit)` sẽ trả về `GIT_OBJ_COMMIT`, vì vậy an toàn để ép kiểu sang một con trỏ `git_commit`.
Khối tiếp theo cho thấy cách truy cập các thuộc tính của commit.
Dòng cuối cùng ở đây sử dụng một loại `git_oid`; đây là biểu diễn của Libgit2 cho một hash SHA-1.
Từ mẫu này, một vài mẫu đã bắt đầu xuất hiện:
* Nếu bạn khai báo một con trỏ và truyền một tham chiếu đến nó vào một cuộc gọi Libgit2, cuộc gọi đó có thể sẽ trả về một mã lỗi số nguyên.
Giá trị `0` chỉ ra thành công; bất cứ thứ gì nhỏ hơn là một lỗi.
* Nếu Libgit2 điền một con trỏ cho bạn, bạn chịu trách nhiệm giải phóng nó.
* Nếu Libgit2 trả về một con trỏ `const` từ một cuộc gọi, bạn không phải giải phóng nó, nhưng nó sẽ trở nên không hợp lệ khi đối tượng mà nó thuộc về được giải phóng.
* Viết C hơi đau đớn.
(((Ruby)))
Cái cuối cùng đó có nghĩa là không có khả năng bạn sẽ viết C khi sử dụng Libgit2.
May mắn thay, có một số binding ngôn ngữ cụ thể có sẵn giúp khá dễ dàng làm việc với các kho chứa Git từ ngôn ngữ và môi trường cụ thể của bạn.
Hãy xem ví dụ trên được viết bằng cách sử dụng các binding Ruby cho Libgit2, được đặt tên là Rugged, và có thể được tìm thấy tại https://github.com/libgit2/rugged[^].
[source,ruby]
----
repo = Rugged::Repository.new('path/to/repository')
commit = repo.head.target
puts commit.message
puts "#{commit.author[:name]} <#{commit.author[:email]}>"
tree = commit.tree
----
Như bạn có thể thấy, mã ít lộn xộn hơn nhiều.
Thứ nhất, Rugged sử dụng các ngoại lệ; nó có thể ném những thứ như `ConfigError` hoặc `ObjectError` để báo hiệu các điều kiện lỗi.
Thứ hai, không có giải phóng tài nguyên rõ ràng, vì Ruby được thu gom rác.
Hãy xem một ví dụ phức tạp hơn một chút: tạo một commit từ đầu
[source,ruby]
----
blob_id = repo.write("Blob contents", :blob) (1)
index = repo.index
index.read_tree(repo.head.target.tree)
index.add(:path => 'newfile.txt', :oid => blob_id) (2)
sig = {
:email => "bob@example.com",
:name => "Bob User",
:time => Time.now,
}
commit_id = Rugged::Commit.create(repo,
:tree => index.write_tree(repo), (3)
:author => sig,
:committer => sig, (4)
:message => "Add newfile.txt", (5)
:parents => repo.empty? ? [] : [ repo.head.target ].compact, (6)
:update_ref => 'HEAD', (7)
)
commit = repo.lookup(commit_id) (8)
----
<1> Tạo một blob mới, chứa nội dung của một tập tin mới.
<2> Điền index với tree của commit head, và thêm tập tin mới tại đường dẫn `newfile.txt`.
<3> Điều này tạo một tree mới trong ODB, và sử dụng nó cho commit mới.
<4> Chúng ta sử dụng cùng một chữ ký cho cả trường author và committer.
<5> Thông điệp commit.
<6> Khi tạo một commit, bạn phải chỉ định các cha mẹ của commit mới.
Điều này sử dụng đỉnh của HEAD cho cha mẹ duy nhất.
<7> Rugged (và Libgit2) có thể tùy chọn cập nhật một tham chiếu khi tạo một commit.
<8> Giá trị trả về là hash SHA-1 của một đối tượng commit mới, sau đó bạn có thể sử dụng để lấy một đối tượng `Commit`.
Mã Ruby tốt đẹp và sạch sẽ, nhưng vì Libgit2 đang làm công việc nặng nhọc, mã này cũng sẽ chạy khá nhanh.
Nếu bạn không phải là một rubyist, chúng tôi đề cập đến một số binding khác trong <<_libgit2_bindings>>.
==== Chức năng Nâng cao
Libgit2 có một vài khả năng nằm ngoài phạm vi của Git cốt lõi.
Một ví dụ là khả năng cắm: Libgit2 cho phép bạn cung cấp các "`backend`" tùy chỉnh cho một số loại thao tác, vì vậy bạn có thể lưu trữ mọi thứ theo cách khác với Git cổ điển.
Libgit2 cho phép các backend tùy chỉnh cho cấu hình, lưu trữ ref, và cơ sở dữ liệu đối tượng, trong số những thứ khác.
Hãy xem cách này hoạt động.
Mã dưới đây được mượn từ bộ ví dụ backend được cung cấp bởi nhóm Libgit2 (có thể được tìm thấy tại https://github.com/libgit2/libgit2-backends[^]).
Đây là cách một backend tùy chỉnh cho cơ sở dữ liệu đối tượng được thiết lập:
[source,c]
----
git_odb *odb;
int error = git_odb_new(&odb); (1)
git_odb_backend *my_backend;
error = git_odb_backend_mine(&my_backend, /*…*/); (2)
error = git_odb_add_backend(odb, my_backend, 1); (3)
git_repository *repo;
error = git_repository_open(&repo, "some-path");
error = git_repository_set_odb(repo, odb); (4)
----
_Lưu ý rằng các lỗi được bắt, nhưng không được xử lý. Chúng tôi hy vọng mã của bạn tốt hơn của chúng tôi._
<1> Khởi tạo một "`frontend`" cơ sở dữ liệu đối tượng (ODB) trống, sẽ hoạt động như một container cho các "`backend`" là những cái thực hiện công việc thực sự.
<2> Khởi tạo một backend ODB tùy chỉnh.
<3> Thêm backend vào frontend.
<4> Mở một kho chứa, và đặt nó sử dụng ODB của chúng ta để tra cứu các đối tượng.
Nhưng `git_odb_backend_mine` này là cái gì?
Chà, đó là constructor cho triển khai ODB của riêng bạn, và bạn có thể làm bất cứ điều gì bạn muốn ở đó, miễn là bạn điền cấu trúc `git_odb_backend` đúng cách.
Đây là những gì nó _có thể_ trông như thế nào:
[source,c]
----
typedef struct {
git_odb_backend parent;
// Some other stuff
void *custom_context;
} my_backend_struct;
int git_odb_backend_mine(git_odb_backend **backend_out, /*…*/)
{
my_backend_struct *backend;
backend = calloc(1, sizeof (my_backend_struct));
backend->custom_context = …;
backend->parent.read = &my_backend__read;
backend->parent.read_prefix = &my_backend__read_prefix;
backend->parent.read_header = &my_backend__read_header;
// …
*backend_out = (git_odb_backend *) backend;
return GIT_SUCCESS;
}
----
Ràng buộc tinh tế nhất ở đây là thành viên đầu tiên của ``my_backend_struct`` phải là một cấu trúc ``git_odb_backend``; điều này đảm bảo rằng bố cục bộ nhớ là những gì mã Libgit2 mong đợi.
Phần còn lại là tùy ý; cấu trúc này có thể lớn hoặc nhỏ như bạn cần.
Hàm khởi tạo phân bổ một số bộ nhớ cho cấu trúc, thiết lập ngữ cảnh tùy chỉnh, và sau đó điền các thành viên của cấu trúc `parent` mà nó hỗ trợ.
Hãy xem tập tin `include/git2/sys/odb_backend.h` trong mã nguồn Libgit2 để biết một bộ chữ ký cuộc gọi đầy đủ; trường hợp sử dụng cụ thể của bạn sẽ giúp xác định cái nào trong số này bạn sẽ muốn hỗ trợ.
[[_libgit2_bindings]]
==== Các Binding Khác
Libgit2 có binding cho nhiều ngôn ngữ.
Ở đây chúng tôi hiển thị một ví dụ nhỏ sử dụng một vài gói binding hoàn chỉnh hơn tính đến thời điểm viết này; các thư viện tồn tại cho nhiều ngôn ngữ khác, bao gồm C++, Go, Node.js, Erlang, và JVM, tất cả ở các giai đoạn trưởng thành khác nhau.
Bộ sưu tập chính thức của các binding có thể được tìm thấy bằng cách duyệt các kho chứa tại https://github.com/libgit2[^].
Mã chúng tôi sẽ viết sẽ trả về thông điệp commit từ commit cuối cùng được trỏ đến bởi HEAD (giống như `git log -1`).
===== LibGit2Sharp
(((.NET)))(((C#)))(((Mono)))
Nếu bạn đang viết một ứng dụng .NET hoặc Mono, LibGit2Sharp (https://github.com/libgit2/libgit2sharp[^]) là những gì bạn đang tìm kiếm.
Các binding được viết bằng C#, và sự chăm sóc lớn đã được thực hiện để bọc các cuộc gọi Libgit2 thô với các API CLR cảm giác tự nhiên.
Đây là những gì chương trình ví dụ của chúng tôi trông như thế nào:
[source,csharp]
----
new Repository(@"C:\\path\\to\\repo").Head.Tip.Message;
----
Đối với các ứng dụng Windows desktop, thậm chí còn có một gói NuGet sẽ giúp bạn bắt đầu nhanh chóng.
===== objective-git
(((Apple)))(((Objective-C)))(((Cocoa)))
Nếu ứng dụng của bạn đang chạy trên một nền tảng Apple, bạn có thể đang sử dụng Objective-C làm ngôn ngữ triển khai của mình.
Objective-Git (https://github.com/libgit2/objective-git[^]) là tên của các binding Libgit2 cho môi trường đó.
Chương trình ví dụ trông như thế này:
[source,objc]
----
GTRepository *repo =
[[GTRepository alloc] initWithURL:[NSURL fileURLWithPath: @"/path/to/repo"] error:NULL];
NSString *msg = [[[repo headReferenceWithError:NULL] resolvedTarget] message];
----
Objective-git hoàn toàn tương tác với Swift, vì vậy đừng sợ nếu bạn đã rời bỏ Objective-C.
===== pygit2
(((Python)))
Các binding cho Libgit2 trong Python được gọi là Pygit2, và có thể được tìm thấy tại https://www.pygit2.org[^].
Chương trình ví dụ của chúng tôi:
[source,python]
----
pygit2.Repository("/path/to/repo") # open repository
.head # get the current branch
.peel(pygit2.Commit) # walk down to the commit
.message # read the message
----
==== Đọc thêm
Tất nhiên, một xử lý đầy đủ về khả năng của Libgit2 nằm ngoài phạm vi của cuốn sách này.
Nếu bạn muốn thêm thông tin về chính Libgit2, có tài liệu API tại https://libgit2.github.com/libgit2[^], và một bộ hướng dẫn tại https://libgit2.github.com/docs[^].
Đối với các binding khác, hãy kiểm tra README và các bài kiểm tra đi kèm; thường có các hướng dẫn nhỏ và con trỏ đến việc đọc thêm ở đó.
=== JGit
(((jgit)))(((Java)))
Nếu bạn muốn sử dụng Git từ trong một chương trình Java, có một thư viện Git đầy đủ tính năng gọi là JGit.
JGit là một triển khai tương đối đầy đủ tính năng của Git được viết bản địa bằng Java, và được sử dụng rộng rãi trong cộng đồng Java.
Dự án JGit nằm dưới ô Eclipse, và trang chủ của nó có thể được tìm thấy tại https://www.eclipse.org/jgit/[^].
==== Thiết lập
Có một số cách để kết nối dự án của bạn với JGit và bắt đầu viết mã chống lại nó.
Có lẽ cách dễ nhất là sử dụng Maven – tích hợp được hoàn thành bằng cách thêm đoạn mã sau vào thẻ `<dependencies>` trong tập tin `pom.xml` của bạn:
[source,xml]
----
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>3.5.0.201409260305-r</version>
</dependency>
----
`version` rất có thể đã tiến bộ vào thời điểm bạn đọc điều này; kiểm tra https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit[^] để biết thông tin kho chứa cập nhật.
Khi bước này hoàn tất, Maven sẽ tự động lấy và sử dụng các thư viện JGit mà bạn cần.
Nếu bạn muốn tự quản lý các phụ thuộc nhị phân, các tập tin nhị phân JGit được xây dựng sẵn có sẵn từ https://www.eclipse.org/jgit/download[^].
Bạn có thể xây dựng chúng vào dự án của mình bằng cách chạy một lệnh như thế này:
[source,console]
----
javac -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App.java
java -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App
----
==== Plumbing
JGit có hai cấp độ API cơ bản: plumbing và porcelain.
Thuật ngữ cho những thứ này đến từ chính Git, và JGit được chia thành khoảng cùng loại khu vực: các API porcelain là một giao diện thân thiện cho các hành động cấp người dùng phổ biến (các loại thứ mà một người dùng bình thường sẽ sử dụng công cụ dòng lệnh Git cho), trong khi các API plumbing dành cho tương tác với các đối tượng kho chứa cấp thấp trực tiếp.
Điểm khởi đầu cho hầu hết các phiên JGit là lớp `Repository`, và điều đầu tiên bạn sẽ muốn làm là tạo một instance của nó.
Đối với một kho chứa dựa trên hệ thống tập tin (có, JGit cho phép các mô hình lưu trữ khác), điều này được thực hiện bằng cách sử dụng `FileRepositoryBuilder`:
[source,java]
----
// Create a new repository
Repository newlyCreatedRepo = FileRepositoryBuilder.create(
new File("/tmp/new_repo/.git"));
newlyCreatedRepo.create();
// Open an existing repository
Repository existingRepo = new FileRepositoryBuilder()
.setGitDir(new File("my_repo/.git"))
.build();
----
Builder có một API trôi chảy để cung cấp tất cả những thứ nó cần để tìm một kho chứa Git, cho dù chương trình của bạn có biết chính xác nó nằm ở đâu hay không.
Nó có thể sử dụng các biến môi trường (`.readEnvironment()`), bắt đầu từ một nơi trong thư mục làm việc và tìm kiếm (`.setWorkTree(…).findGitDir()`), hoặc chỉ mở một thư mục `.git` đã biết như trên.
Khi bạn có một instance `Repository`, bạn có thể làm tất cả các loại thứ với nó.
Đây là một mẫu nhanh:
[source,java]
----
// Get a reference
Ref master = repo.getRef("master");
// Get the object the reference points to
ObjectId masterTip = master.getObjectId();
// Rev-parse
ObjectId obj = repo.resolve("HEAD^{tree}");
// Load raw object contents
ObjectLoader loader = repo.open(masterTip);
loader.copyTo(System.out);
// Create a branch
RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1");
createBranch1.setNewObjectId(masterTip);
createBranch1.update();
// Delete a branch
RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1");
deleteBranch1.setForceUpdate(true);
deleteBranch1.delete();
// Config
Config cfg = repo.getConfig();
String name = cfg.getString("user", null, "name");
----
Có khá nhiều thứ đang diễn ra ở đây, vì vậy hãy đi qua từng phần một.
Dòng đầu tiên lấy một con trỏ đến tham chiếu `master`.
JGit tự động lấy tham chiếu `master` _thực tế_, sống tại `refs/heads/master`, và trả về một đối tượng cho phép bạn lấy thông tin về tham chiếu.
Bạn có thể lấy tên (`.getName()`), và hoặc đối tượng đích của một tham chiếu trực tiếp (`.getObjectId()`) hoặc tham chiếu được trỏ đến bởi một ref tượng trưng (`.getTarget()`).
Các đối tượng Ref cũng được sử dụng để đại diện cho các ref và đối tượng tag, vì vậy bạn có thể hỏi nếu tag là "`peeled`", có nghĩa là nó trỏ đến mục tiêu cuối cùng của một chuỗi (có khả năng dài) các đối tượng tag.
Dòng thứ hai lấy mục tiêu của tham chiếu `master`, được trả về dưới dạng một instance ObjectId.
ObjectId đại diện cho hash SHA-1 của một đối tượng, có thể hoặc có thể không tồn tại trong cơ sở dữ liệu đối tượng của Git.
Dòng thứ ba tương tự, nhưng cho thấy JGit xử lý cú pháp rev-parse như thế nào (để biết thêm về điều này, xem <<ch07-git-tools#_branch_references>>); bạn có thể truyền bất kỳ bộ chỉ định đối tượng nào mà Git hiểu, và JGit sẽ trả về hoặc một ObjectId hợp lệ cho đối tượng đó, hoặc `null`.
Hai dòng tiếp theo cho thấy cách tải nội dung thô của một đối tượng.
Trong ví dụ này, chúng ta gọi `ObjectLoader.copyTo()` để truyền nội dung của đối tượng trực tiếp đến stdout, nhưng ObjectLoader cũng có các phương thức để đọc loại và kích thước của một đối tượng, cũng như trả về nó dưới dạng một mảng byte.
Đối với các đối tượng lớn (trong đó `.isLarge()` trả về `true`), bạn có thể gọi `.openStream()` để lấy một đối tượng giống InputStream có thể đọc dữ liệu đối tượng thô mà không kéo tất cả vào bộ nhớ cùng một lúc.
Vài dòng tiếp theo cho thấy những gì cần để tạo một nhánh mới.
Chúng ta tạo một instance RefUpdate, cấu hình một số tham số, và gọi `.update()` để kích hoạt thay đổi.
Ngay sau đó là mã để xóa cùng nhánh đó.
Lưu ý rằng `.setForceUpdate(true)` là bắt buộc để điều này hoạt động; nếu không cuộc gọi `.delete()` sẽ trả về `REJECTED`, và không có gì sẽ xảy ra.
Ví dụ cuối cùng cho thấy cách lấy giá trị `user.name` từ các tập tin cấu hình Git.
Instance Config này sử dụng kho chứa chúng ta đã mở trước đó cho cấu hình cục bộ, nhưng sẽ tự động phát hiện các tập tin cấu hình toàn cục và hệ thống và đọc các giá trị từ chúng.
Đây chỉ là một mẫu nhỏ của API plumbing đầy đủ; có nhiều phương thức và lớp hơn nữa có sẵn.
Cũng không được hiển thị ở đây là cách JGit xử lý lỗi, đó là thông qua việc sử dụng các ngoại lệ.
Các API JGit đôi khi ném các ngoại lệ Java tiêu chuẩn (chẳng hạn như `IOException`), nhưng có một loạt các loại ngoại lệ cụ thể của JGit cũng được cung cấp (chẳng hạn như `NoRemoteRepositoryException`, `CorruptObjectException`, và `NoMergeBaseException`).
==== Porcelain
Các API plumbing khá hoàn chỉnh, nhưng có thể cồng kềnh để xâu chuỗi chúng lại với nhau để đạt được các mục tiêu chung, như thêm một tập tin vào index, hoặc tạo một commit mới.
JGit cung cấp một bộ API cấp cao hơn để giúp đỡ với điều này, và điểm vào cho các API này là lớp `Git`:
[source,java]
----
Repository repo;
// construct repo...
Git git = new Git(repo);
----
Lớp Git có một bộ các phương thức kiểu _builder_ cấp cao tốt đẹp có thể được sử dụng để xây dựng một số hành vi khá phức tạp.
Hãy xem một ví dụ -- làm một cái gì đó như `git ls-remote`:
[source,java]
----
CredentialsProvider cp = new UsernamePasswordCredentialsProvider("username", "p4ssw0rd");
Collection<Ref> remoteRefs = git.lsRemote()
.setCredentialsProvider(cp)
.setRemote("origin")
.setTags(true)
.setHeads(false)
.call();
for (Ref ref : remoteRefs) {
System.out.println(ref.getName() + " -> " + ref.getObjectId().name());
}
----
Đây là một mẫu phổ biến với lớp Git; các phương thức trả về một đối tượng lệnh cho phép bạn xâu chuỗi các cuộc gọi phương thức để đặt các tham số, được thực thi khi bạn gọi `.call()`.
Trong trường hợp này, chúng ta đang yêu cầu remote `origin` cho các tag, nhưng không phải head.
Cũng lưu ý việc sử dụng một đối tượng `CredentialsProvider` để xác thực.
Nhiều lệnh khác có sẵn thông qua lớp Git, bao gồm nhưng không giới hạn ở `add`, `blame`, `commit`, `clean`, `push`, `rebase`, `revert`, và `reset`.
==== Đọc thêm
Đây chỉ là một mẫu nhỏ của khả năng đầy đủ của JGit.
Nếu bạn quan tâm và muốn tìm hiểu thêm, đây là nơi để tìm kiếm thông tin và cảm hứng:
* Tài liệu API JGit chính thức có thể được tìm thấy tại https://www.eclipse.org/jgit/documentation[^].
Đây là Javadoc tiêu chuẩn, vì vậy JVM IDE yêu thích của bạn sẽ có thể cài đặt chúng cục bộ.
* JGit Cookbook tại https://github.com/centic9/jgit-cookbook[^] có nhiều ví dụ về cách thực hiện các nhiệm vụ cụ thể với JGit.
=== go-git
(((go-git)))(((Go)))
Trong trường hợp bạn muốn tích hợp Git vào một dịch vụ được viết bằng Golang, cũng có một triển khai thư viện Go thuần túy.
Triển khai này không có bất kỳ phụ thuộc gốc nào và do đó không dễ bị lỗi quản lý bộ nhớ thủ công.
Nó cũng minh bạch cho các công cụ phân tích hiệu suất Golang tiêu chuẩn như CPU, Memory profilers, race detector, v.v.
go-git tập trung vào khả năng mở rộng, tương thích và hỗ trợ hầu hết các API plumbing, được ghi lại tại https://github.com/go-git/go-git/blob/master/COMPATIBILITY.md[^].
Đây là một ví dụ cơ bản về việc sử dụng API Go:
[source, go]
----
import "github.com/go-git/go-git/v5"
r, err := git.PlainClone("/tmp/foo", false, &git.CloneOptions{
URL: "https://github.com/go-git/go-git",
Progress: os.Stdout,
})
----
Ngay khi bạn có một instance `Repository`, bạn có thể truy cập thông tin và thực hiện các thay đổi trên nó:
[source, go]
----
// retrieves the branch pointed by HEAD
ref, err := r.Head()
// get the commit object, pointed by ref
commit, err := r.CommitObject(ref.Hash())
// retrieves the commit history
history, err := commit.History()
// iterates over the commits and print each
for _, c := range history {
fmt.Println(c)
}
----
==== Chức năng Nâng cao
go-git có một vài tính năng nâng cao đáng chú ý, một trong số đó là hệ thống lưu trữ có thể cắm, tương tự như các backend Libgit2.
Triển khai mặc định là lưu trữ trong bộ nhớ, rất nhanh.
[source, go]
----
r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
URL: "https://github.com/go-git/go-git",
})
----
Lưu trữ có thể cắm cung cấp nhiều tùy chọn thú vị.
Ví dụ, https://github.com/go-git/go-git/tree/master/_examples/storage[^] cho phép bạn lưu trữ các tham chiếu, đối tượng, và cấu hình trong cơ sở dữ liệu Aerospike.
Một tính năng khác là trừu tượng hóa hệ thống tập tin linh hoạt.
Sử dụng https://pkg.go.dev/github.com/go-git/go-billy/v5?tab=doc#Filesystem[^] rất dễ dàng để lưu trữ tất cả các tập tin theo cách khác nhau, tức là bằng cách đóng gói tất cả chúng vào một kho lưu trữ duy nhất trên đĩa hoặc bằng cách giữ tất cả chúng trong bộ nhớ.
Một trường hợp sử dụng nâng cao khác bao gồm một máy khách HTTP có thể điều chỉnh tốt, chẳng hạn như cái được tìm thấy tại https://github.com/go-git/go-git/blob/master/_examples/custom_http/main.go[^].
[source, go]
----
customClient := &http.Client{
Transport: &http.Transport{ // accept any certificate (might be useful for testing)
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: 15 * time.Second, // 15 second timeout
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // don't follow redirect
},
}
// Override http(s) default protocol to use our custom client
client.InstallProtocol("https", githttp.NewClient(customClient))
// Clone repository using the new client if the protocol is https://
r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{URL: url})
----
==== Đọc thêm
Một xử lý đầy đủ về khả năng của go-git nằm ngoài phạm vi của cuốn sách này.
Nếu bạn muốn thêm thông tin về go-git, có tài liệu API tại https://pkg.go.dev/github.com/go-git/go-git/v5[^], và một bộ ví dụ sử dụng tại https://github.com/go-git/go-git/tree/master/_examples[^].
=== Dulwich
(((Dulwich)))(((Python)))
Cũng có một triển khai Git thuần Python - Dulwich.
Dự án được lưu trữ tại https://www.dulwich.io/[^].
Nó nhằm cung cấp một giao diện cho các kho chứa Git (cả cục bộ và từ xa) không gọi trực tiếp đến Git mà thay vào đó sử dụng Python thuần túy.
Tuy nhiên, nó có các extension C tùy chọn, cải thiện đáng kể hiệu suất.
Dulwich tuân theo thiết kế Git và tách hai cấp độ API cơ bản: plumbing và porcelain.
Đây là một ví dụ sử dụng API cấp thấp hơn để truy cập thông điệp commit của commit cuối cùng:
[source, python]
----
from dulwich.repo import Repo
r = Repo('.')
r.head()
# '57fbe010446356833a6ad1600059d80b1e731e15'
c = r[r.head()]
c
# <Commit 015fc1267258458901a94d228e39f0a378370466>
c.message
# 'Add note about encoding.\\n'
----
Để in nhật ký commit bằng API porcelain cấp cao, người ta có thể sử dụng:
[source, python]
----
from dulwich import porcelain
porcelain.log('.', max_entries=1)
#commit: 57fbe010446356833a6ad1600059d80b1e731e15
#Author: Jelmer Vernooij <jelmer@jelmer.uk>
#Date: Sat Apr 29 2017 23:57:34 +0000
----
==== Đọc thêm
Tài liệu API, hướng dẫn, và nhiều ví dụ về cách thực hiện các nhiệm vụ cụ thể với Dulwich có sẵn trên trang web chính thức https://www.dulwich.io[^].
[[C-git-commands]]
[appendix]
== Các Lệnh Git
Trong suốt cuốn sách, chúng tôi đã giới thiệu hàng chục lệnh Git và đã cố gắng hết sức để giới thiệu chúng trong một câu chuyện, thêm nhiều lệnh vào câu chuyện một cách từ từ.
Tuy nhiên, điều này khiến các ví dụ về việc sử dụng các lệnh nằm rải rác trong toàn bộ cuốn sách.
Trong phụ lục này, chúng tôi sẽ đi qua tất cả các lệnh Git mà chúng tôi đã đề cập trong suốt cuốn sách, được nhóm lại đại khái theo mục đích sử dụng của chúng.
Chúng tôi sẽ nói về những gì mỗi lệnh thực hiện một cách tổng quát và sau đó chỉ ra nơi trong cuốn sách bạn có thể tìm thấy chúng tôi đã sử dụng nó.
[TIP]
====
Bạn có thể viết tắt các tùy chọn dài.
Ví dụ: bạn có thể nhập `git commit --a`, hoạt động giống như khi bạn nhập `git commit --amend`.
Điều này chỉ hoạt động khi các chữ cái sau `--` là duy nhất cho một tùy chọn.
Hãy sử dụng tùy chọn đầy đủ khi viết các tập lệnh.
====
=== Cài đặt và Cấu hình
Có hai lệnh được sử dụng khá nhiều, từ những lần gọi Git đầu tiên đến việc tinh chỉnh và tham khảo thông thường hàng ngày, đó là các lệnh `config` và `help`.
==== git config
Git có một cách mặc định để làm hàng trăm thứ.
Đối với rất nhiều thứ trong số này, bạn có thể bảo Git mặc định làm chúng theo một cách khác, hoặc thiết lập các tùy chọn của bạn.
Điều này bao gồm mọi thứ từ việc cho Git biết tên của bạn là gì đến các tùy chọn màu sắc cụ thể của thiết bị đầu cuối hoặc trình soạn thảo bạn sử dụng.
Có một số tệp mà lệnh này sẽ đọc và ghi vào để bạn có thể đặt các giá trị trên toàn cầu hoặc xuống các kho lưu trữ cụ thể.
Lệnh `git config` đã được sử dụng trong gần như mọi chương của cuốn sách.
Trong <<ch01-getting-started#_first_time>>, chúng tôi đã sử dụng nó để chỉ định tên, địa chỉ email và trình soạn thảo ưu tiên của chúng tôi trước khi chúng tôi bắt đầu sử dụng Git.
Trong <<ch02-git-basics-chapter#_git_aliases>>, chúng tôi đã chỉ ra cách bạn có thể sử dụng nó để tạo các lệnh viết tắt mở rộng thành các chuỗi tùy chọn dài để bạn không phải nhập chúng mỗi lần.
Trong <<ch03-git-branching#_rebasing>>, chúng tôi đã sử dụng nó để đặt `--rebase` làm mặc định khi bạn chạy `git pull`.
Trong <<ch07-git-tools#_credential_caching>>, chúng tôi đã sử dụng nó để thiết lập một kho lưu trữ mặc định cho mật khẩu HTTP của bạn.
Trong <<ch08-customizing-git#_keyword_expansion>>, chúng tôi đã chỉ ra cách thiết lập các bộ lọc smudge và clean trên nội dung vào và ra khỏi Git.
Cuối cùng, về cơ bản toàn bộ <<ch08-customizing-git#_git_config>> được dành riêng cho lệnh này.
[[ch_core_editor]]
==== các lệnh git config core.editor
Đi kèm với các hướng dẫn cấu hình trong <<ch01-getting-started#_editor>>, nhiều trình soạn thảo có thể được thiết lập như sau:
.Danh sách đầy đủ các lệnh cấu hình `core.editor`
[cols="1,2",options="header"]
|==============================
|Trình soạn thảo | Lệnh cấu hình
|Atom |`git config --global core.editor "atom --wait"`
|BBEdit (macOS, với các công cụ dòng lệnh) |`git config --global core.editor "bbedit -w"`
|Emacs |`git config --global core.editor emacs`
|Gedit (Linux) |`git config --global core.editor "gedit --wait --new-window"`
|Gvim (Windows 64-bit) |`git config --global core.editor "'C:\Program Files\Vim\vim72\gvim.exe' --nofork '%*'"` (Xem thêm lưu ý bên dưới)
|Helix |`git config --global core.editor "hx"`
|Kate (Linux) |`git config --global core.editor "kate --block"`
|nano |`git config --global core.editor "nano -w"`
|Notepad (Windows 64-bit) |`git config core.editor notepad`
|Notepad++ (Windows 64-bit) |`git config --global core.editor "'C:\Program Files\Notepad+\+\notepad++.exe' -multiInst -notabbar -nosession -noPlugin"` (Xem thêm lưu ý bên dưới)
|Scratch (Linux)|`git config --global core.editor "scratch-text-editor"`
|Sublime Text (macOS) |`git config --global core.editor "/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl --new-window --wait"`
|Sublime Text (Windows 64-bit) |`git config --global core.editor "'C:\Program Files\Sublime Text 3\sublime_text.exe' -w"` (Xem thêm lưu ý bên dưới)
|TextEdit (macOS)|`git config --global core.editor "open --wait-apps --new -e"`
|Textmate |`git config --global core.editor "mate -w"`
|Textpad (Windows 64-bit) |`git config --global core.editor "'C:\Program Files\TextPad 5\TextPad.exe' -m"` (Xem thêm lưu ý bên dưới)
|UltraEdit (Windows 64-bit) | `git config --global core.editor Uedit32`
|Vim |`git config --global core.editor "vim --nofork"`
|Visual Studio Code |`git config --global core.editor "code --wait"`
|VSCodium (Phần mềm nguồn mở miễn phí/tự do của VSCode) | `git config --global core.editor "codium --wait"`
|WordPad |`git config --global core.editor "'C:\Program Files\Windows NT\Accessories\wordpad.exe'"`
|Xi | `git config --global core.editor "xi --wait"`
|==============================
[NOTE]
====
Nếu bạn có trình soạn thảo 32-bit trên hệ thống Windows 64-bit, chương trình sẽ được cài đặt trong `C:\Program Files (x86)\` thay vì `C:\Program Files\` như trong bảng trên.
====
==== git help
Lệnh `git help` được sử dụng để hiển thị cho bạn tất cả các tài liệu đi kèm với Git về bất kỳ lệnh nào.
Mặc dù chúng tôi đang đưa ra một cái nhìn tổng quan sơ bộ về hầu hết các lệnh phổ biến hơn trong phụ lục này, để có danh sách đầy đủ tất cả các tùy chọn và cờ có thể cho mọi lệnh, bạn luôn có thể chạy `git help <command>`.
Chúng tôi đã giới thiệu lệnh `git help` trong <<ch01-getting-started#_git_help>> và chỉ cho bạn cách sử dụng nó để tìm thêm thông tin về `git shell` trong <<ch04-git-on-the-server#_setting_up_server>>.
=== Nhận và Tạo Dự án
Có hai cách để có được một kho lưu trữ Git.
Một là sao chép nó từ một kho lưu trữ hiện có trên mạng hoặc nơi khác và hai là tạo một cái mới trong một thư mục hiện có.
==== git init
Để lấy một thư mục và biến nó thành một kho lưu trữ Git mới để bạn có thể bắt đầu kiểm soát phiên bản nó, bạn chỉ cần chạy `git init`.
Chúng tôi giới thiệu điều này lần đầu tiên trong <<ch02-git-basics-chapter#_getting_a_repo>>, nơi chúng tôi chỉ ra việc tạo một kho lưu trữ hoàn toàn mới để bắt đầu làm việc.
Chúng tôi nói ngắn gọn về cách bạn có thể thay đổi tên nhánh mặc định từ "`master`" trong <<ch03-git-branching#_remote_branches>>.
Chúng tôi sử dụng lệnh này để tạo một kho lưu trữ trần (bare repository) trống cho một máy chủ trong <<ch04-git-on-the-server#_bare_repo>>.
Cuối cùng, chúng tôi đi qua một số chi tiết về những gì nó thực sự làm ở hậu trường trong <<ch10-git-internals#_plumbing_porcelain>>.
==== git clone
Lệnh `git clone` thực sự là một trình bao bọc xung quanh một số lệnh khác.
Nó tạo một thư mục mới, đi vào đó và chạy `git init` để biến nó thành một kho lưu trữ Git trống, thêm một điều khiển từ xa (`git remote add`) vào URL mà bạn chuyển cho nó (theo mặc định được đặt tên là `origin`), chạy `git fetch` từ kho lưu trữ từ xa đó và sau đó kiểm xuất (checkout) cam kết mới nhất vào thư mục làm việc của bạn với `git checkout`.
Lệnh `git clone` được sử dụng ở hàng chục nơi trong suốt cuốn sách, nhưng chúng tôi sẽ chỉ liệt kê một vài nơi thú vị.
Về cơ bản, nó được giới thiệu và giải thích trong <<ch02-git-basics-chapter#_git_cloning>>, nơi chúng tôi đi qua một vài ví dụ.
Trong <<ch04-git-on-the-server#_getting_git_on_a_server>>, chúng tôi xem xét việc sử dụng tùy chọn `--bare` để tạo một bản sao của kho lưu trữ Git không có thư mục làm việc.
Trong <<ch07-git-tools#_bundling>>, chúng tôi sử dụng nó để giải nén một kho lưu trữ Git đã được đóng gói.
Cuối cùng, trong <<ch07-git-tools#_cloning_submodules>>, chúng tôi tìm hiểu tùy chọn `--recurse-submodules` để làm cho việc sao chép một kho lưu trữ với các mô-đun con đơn giản hơn một chút.
Mặc dù nó được sử dụng ở nhiều nơi khác trong cuốn sách, nhưng đây là những nơi có phần độc đáo hoặc nơi nó được sử dụng theo những cách hơi khác một chút.
=== Snapshot Cơ bản
Đối với quy trình làm việc cơ bản của việc tổ chức nội dung (staging) và cam kết (committing) nó vào lịch sử của bạn, chỉ có một vài lệnh cơ bản.
==== git add
Lệnh `git add` thêm nội dung từ thư mục làm việc vào khu vực tổ chức (hoặc "`index`") cho cam kết tiếp theo.
Khi lệnh `git commit` được chạy, theo mặc định, nó chỉ nhìn vào khu vực tổ chức này, vì vậy `git add` được sử dụng để tạo ra chính xác những gì bạn muốn ảnh chụp nhanh cam kết tiếp theo của mình trông như thế nào.
Lệnh này là một lệnh cực kỳ quan trọng trong Git và được đề cập hoặc sử dụng hàng chục lần trong cuốn sách này.
Chúng tôi sẽ nhanh chóng đề cập đến một số cách sử dụng độc đáo có thể được tìm thấy.
Chúng tôi giới thiệu và giải thích `git add` chi tiết lần đầu tiên trong <<ch02-git-basics-chapter#_tracking_files>>.
Chúng tôi đề cập đến cách sử dụng nó để giải quyết xung đột hợp nhất trong <<ch03-git-branching#_basic_merge_conflicts>>.
Chúng tôi xem xét việc sử dụng nó để tổ chức tương tác chỉ các phần cụ thể của tệp đã sửa đổi trong <<ch07-git-tools#_interactive_staging>>.
Cuối cùng, chúng tôi mô phỏng nó ở cấp độ thấp trong <<ch10-git-internals#_tree_objects>>, để bạn có thể biết nó đang làm gì ở hậu trường.
==== git status
Lệnh `git status` sẽ hiển thị cho bạn các trạng thái khác nhau của các tệp trong thư mục làm việc và khu vực tổ chức của bạn.
Những tệp nào được sửa đổi và chưa được tổ chức và những tệp nào được tổ chức nhưng chưa được cam kết.
Ở dạng bình thường, nó cũng sẽ hiển thị cho bạn một số gợi ý cơ bản về cách di chuyển các tệp giữa các giai đoạn này.
Chúng tôi đề cập đến `status` lần đầu tiên trong <<ch02-git-basics-chapter#_checking_status>>, cả ở dạng cơ bản và đơn giản hóa.
Mặc dù chúng tôi sử dụng nó trong suốt cuốn sách, nhưng hầu như mọi thứ bạn có thể làm với lệnh `git status` đều được đề cập ở đó.
==== git diff
Lệnh `git diff` được sử dụng khi bạn muốn xem sự khác biệt giữa bất kỳ hai cây (tree) nào.
Đây có thể là sự khác biệt giữa môi trường làm việc và khu vực tổ chức của bạn (`git diff` một mình), giữa khu vực tổ chức và cam kết cuối cùng của bạn (`git diff --staged`), hoặc giữa hai cam kết (`git diff master branchB`).
Chúng tôi xem xét các cách sử dụng cơ bản của `git diff` lần đầu tiên trong <<ch02-git-basics-chapter#_git_diff_staged>>, nơi chúng tôi chỉ ra cách xem những thay đổi nào được tổ chức và những thay đổi nào chưa được tổ chức.
Chúng tôi sử dụng nó để tìm các vấn đề về khoảng trắng có thể xảy ra trước khi cam kết với tùy chọn `--check` trong <<ch05-distributed-git#_commit_guidelines>>.
Chúng tôi xem cách kiểm tra sự khác biệt giữa các nhánh hiệu quả hơn với cú pháp `git diff A...B` trong <<ch05-distributed-git#_what_is_introduced>>.
Chúng tôi sử dụng nó để lọc ra các khác biệt về khoảng trắng với `-b` và cách so sánh các giai đoạn khác nhau của các tệp bị xung đột với `--theirs`, `--ours` và `--base` trong <<ch07-git-tools#_advanced_merging>>.
Cuối cùng, chúng tôi sử dụng nó để so sánh hiệu quả các thay đổi mô-đun con với `--submodule` trong <<ch07-git-tools#_starting_submodules>>.
==== git difftool
Lệnh `git difftool` chỉ đơn giản là khởi chạy một công cụ bên ngoài để hiển thị cho bạn sự khác biệt giữa hai cây trong trường hợp bạn muốn sử dụng thứ gì đó khác ngoài lệnh `git diff` tích hợp sẵn.
Chúng tôi chỉ đề cập ngắn gọn về điều này trong <<ch02-git-basics-chapter#_git_diff_staged>>.
==== git commit
Lệnh `git commit` lấy tất cả nội dung tệp đã được tổ chức với `git add` và ghi lại một ảnh chụp nhanh vĩnh viễn mới trong cơ sở dữ liệu và sau đó di chuyển con trỏ nhánh trên nhánh hiện tại lên đó.
Chúng tôi đề cập đến những điều cơ bản về cam kết lần đầu tiên trong <<ch02-git-basics-chapter#_committing_changes>>.
Ở đó, chúng tôi cũng trình bày cách sử dụng cờ `-a` để bỏ qua bước `git add` trong quy trình làm việc hàng ngày và cách sử dụng cờ `-m` để chuyển thông báo cam kết vào dòng lệnh thay vì kích hoạt trình soạn thảo.
Trong <<ch02-git-basics-chapter#_undoing>>, chúng tôi đề cập đến việc sử dụng tùy chọn `--amend` để làm lại cam kết gần đây nhất.
Trong <<ch03-git-branching#_git_branches_overview>>, chúng tôi đi sâu hơn nhiều về những gì `git commit` làm và tại sao nó lại làm như vậy.
Chúng tôi đã xem xét cách ký các cam kết bằng mật mã với cờ `-S` trong <<ch07-git-tools#_signing_commits>>.
Cuối cùng, chúng tôi xem xét những gì lệnh `git commit` làm trong nền và cách nó thực sự được triển khai trong <<ch10-git-internals#_git_commit_objects>>.
==== git reset
Lệnh `git reset` chủ yếu được sử dụng để hoàn tác mọi thứ, như bạn có thể đoán qua động từ.
Nó di chuyển con trỏ `HEAD` và tùy chọn thay đổi `index` hoặc khu vực tổ chức và cũng có thể tùy chọn thay đổi thư mục làm việc nếu bạn sử dụng `--hard`.
Tùy chọn cuối cùng này có thể khiến lệnh này làm mất công việc của bạn nếu sử dụng không đúng cách, vì vậy hãy đảm bảo bạn hiểu nó trước khi sử dụng.
Chúng tôi đề cập hiệu quả đến cách sử dụng đơn giản nhất của `git reset` lần đầu tiên trong <<ch02-git-basics-chapter#_unstaging>>, nơi chúng tôi sử dụng nó để hủy tổ chức một tệp mà chúng tôi đã chạy `git add`.
Sau đó, chúng tôi đề cập đến nó khá chi tiết trong <<ch07-git-tools#_git_reset>>, phần này hoàn toàn dành riêng để giải thích lệnh này.
Chúng tôi sử dụng `git reset --hard` để hủy bỏ hợp nhất trong <<ch07-git-tools#_abort_merge>>, nơi chúng tôi cũng sử dụng `git merge --abort`, đây là một trình bao bọc cho lệnh `git reset`.
==== git rm
Lệnh `git rm` được sử dụng để xóa các tệp khỏi khu vực tổ chức và thư mục làm việc cho Git.
Nó tương tự như `git add` ở chỗ nó tổ chức việc xóa một tệp cho cam kết tiếp theo.
Chúng tôi đề cập đến lệnh `git rm` khá chi tiết trong <<ch02-git-basics-chapter#_removing_files>>, bao gồm xóa đệ quy các tệp và chỉ xóa các tệp khỏi khu vực tổ chức nhưng để chúng trong thư mục làm việc với `--cached`.
Cách sử dụng khác duy nhất của `git rm` trong cuốn sách là trong <<ch10-git-internals#_removing_objects>>, nơi chúng tôi sử dụng và giải thích ngắn gọn về `--ignore-unmatch` khi chạy `git filter-branch`, điều này chỉ đơn giản là làm cho nó không bị lỗi khi tệp chúng tôi đang cố gắng xóa không tồn tại.
Điều này có thể hữu ích cho mục đích viết kịch bản.
==== git mv
Lệnh `git mv` là một lệnh tiện lợi mỏng để di chuyển một tệp và sau đó chạy `git add` trên tệp mới và `git rm` trên tệp cũ.
Chúng tôi chỉ đề cập ngắn gọn về lệnh này trong <<ch02-git-basics-chapter#_git_mv>>.
==== git clean
Lệnh `git clean` được sử dụng để xóa các tệp không mong muốn khỏi thư mục làm việc của bạn.
Điều này có thể bao gồm việc xóa các tạo phẩm xây dựng tạm thời hoặc các tệp xung đột hợp nhất.
Chúng tôi đề cập đến nhiều tùy chọn và kịch bản mà bạn có thể sử dụng lệnh clean trong <<ch07-git-tools#_git_clean>>.
=== Nhánh và Hợp nhất
Chỉ có một số ít lệnh thực hiện hầu hết các chức năng phân nhánh và hợp nhất trong Git.
==== git branch
Lệnh `git branch` thực sự là một công cụ quản lý nhánh.
Nó có thể liệt kê các nhánh bạn có, tạo một nhánh mới, xóa các nhánh và đổi tên các nhánh.
Hầu hết <<ch03-git-branching#ch03-git-branching>> được dành riêng cho lệnh `branch` và nó được sử dụng trong toàn bộ chương.
Chúng tôi giới thiệu nó lần đầu tiên trong <<ch03-git-branching#_create_new_branch>> và chúng tôi đi qua hầu hết các tính năng khác của nó (liệt kê và xóa) trong <<ch03-git-branching#_branch_management>>.
Trong <<ch03-git-branching#_tracking_branches>>, chúng tôi sử dụng tùy chọn `git branch -u` để thiết lập một nhánh theo dõi.
Cuối cùng, chúng tôi đi qua một số điều nó làm trong nền trong <<ch10-git-internals#_git_refs>>.
==== git checkout
Lệnh `git checkout` được sử dụng để chuyển đổi các nhánh và kiểm xuất nội dung vào thư mục làm việc của bạn.
Chúng tôi gặp lệnh này lần đầu tiên trong <<ch03-git-branching#_switching_branches>> cùng với lệnh `git branch`.
Chúng tôi xem cách sử dụng nó để bắt đầu theo dõi các nhánh với cờ `--track` trong <<ch03-git-branching#_tracking_branches>>.
Chúng tôi sử dụng nó để giới thiệu lại các xung đột tệp với `--conflict=diff3` trong <<ch07-git-tools#_checking_out_conflicts>>.
Chúng tôi đi vào chi tiết hơn về mối quan hệ của nó với `git reset` trong <<ch07-git-tools#_git_reset>>.
Cuối cùng, chúng tôi đi vào một số chi tiết triển khai trong <<ch10-git-internals#ref_the_ref>>.
==== git merge
Công cụ `git merge` được sử dụng để hợp nhất một hoặc nhiều nhánh vào nhánh bạn đã kiểm xuất.
Sau đó, nó sẽ nâng nhánh hiện tại lên kết quả của việc hợp nhất.
Lệnh `git merge` được giới thiệu lần đầu tiên trong <<ch03-git-branching#_basic_branching>>.
Mặc dù nó được sử dụng ở nhiều nơi khác nhau trong cuốn sách, nhưng có rất ít biến thể của lệnh `merge` -- thường chỉ là `git merge <branch>` với tên của nhánh duy nhất bạn muốn hợp nhất vào.
Chúng tôi đã đề cập đến cách thực hiện hợp nhất nén (squashed merge) (nơi Git hợp nhất công việc nhưng giả vờ như đó chỉ là một cam kết mới mà không ghi lại lịch sử của nhánh bạn đang hợp nhất vào) ở phần cuối của <<ch05-distributed-git#_public_project>>.
Chúng tôi đã đi qua rất nhiều về quy trình và lệnh hợp nhất, bao gồm lệnh `-Xignore-space-change` và cờ `--abort` để hủy bỏ một hợp nhất có vấn đề trong <<ch07-git-tools#_advanced_merging>>.
Chúng tôi đã học cách xác minh chữ ký trước khi hợp nhất nếu dự án của bạn đang sử dụng ký GPG trong <<ch07-git-tools#_signing_commits>>.
Cuối cùng, chúng tôi đã tìm hiểu về hợp nhất Subtree trong <<ch07-git-tools#_subtree_merge>>.
==== git mergetool
Lệnh `git mergetool` chỉ đơn giản là khởi chạy một trình trợ giúp hợp nhất bên ngoài trong trường hợp bạn gặp sự cố với việc hợp nhất trong Git.
Chúng tôi đề cập nhanh đến nó trong <<ch03-git-branching#_basic_merge_conflicts>> và đi vào chi tiết về cách triển khai công cụ hợp nhất bên ngoài của riêng bạn trong <<ch08-customizing-git#_external_merge_tools>>.
==== git log
Lệnh `git log` được sử dụng để hiển thị lịch sử được ghi lại có thể truy cập của một dự án từ ảnh chụp nhanh cam kết gần đây nhất trở về trước.
Theo mặc định, nó sẽ chỉ hiển thị lịch sử của nhánh bạn đang ở, nhưng có thể được cung cấp các đầu (heads) hoặc nhánh khác nhau hoặc thậm chí nhiều nhánh để duyệt qua.
Nó cũng thường được sử dụng để hiển thị sự khác biệt giữa hai hoặc nhiều nhánh ở cấp độ cam kết.
Lệnh này được sử dụng trong gần như mọi chương của cuốn sách để minh họa lịch sử của một dự án.
Chúng tôi giới thiệu lệnh và đề cập đến nó ở một mức độ sâu nào đó trong <<ch02-git-basics-chapter#_viewing_history>>.
Ở đó, chúng tôi xem xét tùy chọn `-p` và `--stat` để biết những gì đã được giới thiệu trong mỗi cam kết và các tùy chọn `--pretty` và `--oneline` để xem lịch sử ngắn gọn hơn, cùng với một số tùy chọn lọc ngày và tác giả đơn giản.
Trong <<ch03-git-branching#_create_new_branch>>, chúng tôi sử dụng nó với tùy chọn `--decorate` để dễ dàng hình dung vị trí các con trỏ nhánh của chúng tôi và chúng tôi cũng sử dụng tùy chọn `--graph` để xem các lịch sử phân kỳ trông như thế nào.
Trong <<ch05-distributed-git#_private_team>> và <<ch07-git-tools#_commit_ranges>>, chúng tôi đề cập đến cú pháp `branchA..branchB` để sử dụng lệnh `git log` để xem những cam kết nào là duy nhất cho một nhánh so với một nhánh khác.
Trong <<ch07-git-tools#_commit_ranges>>, chúng tôi đi qua điều này khá rộng rãi.
Trong <<ch07-git-tools#_merge_log>> và <<ch07-git-tools#_triple_dot>>, chúng tôi đề cập đến việc sử dụng định dạng `branchA...branchB` và cú pháp `--left-right` để xem những gì có trong nhánh này hoặc nhánh kia nhưng không có trong cả hai.
Trong <<ch07-git-tools#_merge_log>>, chúng tôi cũng xem xét cách sử dụng tùy chọn `--merge` để giúp gỡ lỗi xung đột hợp nhất cũng như sử dụng tùy chọn `--cc` để xem các xung đột cam kết hợp nhất trong lịch sử của bạn.
Trong <<ch07-git-tools#_git_reflog>>, chúng tôi sử dụng tùy chọn `-g` để xem Git reflog thông qua công cụ này thay vì thực hiện duyệt nhánh.
Trong <<ch07-git-tools#_searching>>, chúng tôi xem xét việc sử dụng các tùy chọn `-S` và `-L` để thực hiện các tìm kiếm khá phức tạp cho một cái gì đó đã xảy ra trong lịch sử trong mã như xem lịch sử của một hàm.
Trong <<ch07-git-tools#_signing_commits>>, chúng tôi xem cách sử dụng `--show-signature` để thêm một chuỗi xác thực vào mỗi cam kết trong đầu ra `git log` dựa trên việc nó có được ký hợp lệ hay không.
==== git stash
Lệnh `git stash` được sử dụng để lưu trữ tạm thời công việc chưa được cam kết để dọn sạch thư mục làm việc của bạn mà không cần phải cam kết công việc chưa hoàn thành trên một nhánh.
Điều này về cơ bản được đề cập hoàn toàn trong <<ch07-git-tools#_git_stashing>>.
==== git tag
Lệnh `git tag` được sử dụng để cung cấp một dấu trang vĩnh viễn cho một điểm cụ thể trong lịch sử mã.
Nói chung, điều này được sử dụng cho những thứ như phát hành.
Lệnh này được giới thiệu và đề cập chi tiết trong <<ch02-git-basics-chapter#_git_tagging>> và chúng tôi sử dụng nó trong thực tế trong <<ch05-distributed-git#_tagging_releases>>.
Chúng tôi cũng đề cập đến cách tạo thẻ được ký GPG với cờ `-s` và xác minh thẻ đó với cờ `-v` trong <<ch07-git-tools#_signing>>.
=== Chia sẻ và Cập nhật Dự án
Không có nhiều lệnh trong Git truy cập mạng, gần như tất cả các lệnh đều hoạt động trên cơ sở dữ liệu cục bộ.
Khi bạn đã sẵn sàng chia sẻ công việc của mình hoặc kéo các thay đổi từ nơi khác, có một số lệnh xử lý các kho lưu trữ từ xa.
==== git fetch
Lệnh `git fetch` giao tiếp với một kho lưu trữ từ xa và lấy xuống tất cả thông tin có trong kho lưu trữ đó mà không có trong kho lưu trữ hiện tại của bạn và lưu trữ nó trong cơ sở dữ liệu cục bộ của bạn.
Chúng tôi xem xét lệnh này lần đầu tiên trong <<ch02-git-basics-chapter#_fetching_and_pulling>> và chúng tôi tiếp tục thấy các ví dụ về việc sử dụng nó trong <<ch03-git-branching#_remote_branches>>.
Chúng tôi cũng sử dụng nó trong một số ví dụ trong <<ch05-distributed-git#_contributing_project>>.
Chúng tôi sử dụng nó để lấy một tham chiếu cụ thể duy nhất nằm ngoài không gian mặc định trong <<ch06-github#_pr_refs>> và chúng tôi xem cách lấy từ một gói (bundle) trong <<ch07-git-tools#_bundling>>.
Chúng tôi thiết lập các refspec tùy chỉnh cao để làm cho `git fetch` thực hiện điều gì đó hơi khác so với mặc định trong <<ch10-git-internals#_refspec>>.
==== git pull
Lệnh `git pull` về cơ bản là sự kết hợp của các lệnh `git fetch` và `git merge`, trong đó Git sẽ lấy từ điều khiển từ xa mà bạn chỉ định và sau đó ngay lập tức cố gắng hợp nhất nó vào nhánh bạn đang ở.
Chúng tôi giới thiệu nhanh về nó trong <<ch02-git-basics-chapter#_fetching_and_pulling>> và chỉ ra cách xem những gì nó sẽ hợp nhất nếu bạn chạy nó trong <<ch02-git-basics-chapter#_inspecting_remote>>.
Chúng tôi cũng xem cách sử dụng nó để giúp giải quyết các khó khăn khi rebase trong <<ch03-git-branching#_rebase_rebase>>.
Chúng tôi chỉ ra cách sử dụng nó với một URL để kéo các thay đổi theo kiểu một lần trong <<ch05-distributed-git#_checking_out_remotes>>.
Cuối cùng, chúng tôi đề cập rất nhanh rằng bạn có thể sử dụng tùy chọn `--verify-signatures` cho nó để xác minh rằng các cam kết bạn đang kéo đã được ký GPG trong <<ch07-git-tools#_signing_commits>>.
==== git push
Lệnh `git push` được sử dụng để giao tiếp với một kho lưu trữ khác, tính toán những gì cơ sở dữ liệu cục bộ của bạn có mà kho lưu trữ từ xa không có, và sau đó đẩy sự khác biệt vào kho lưu trữ khác.
Nó yêu cầu quyền ghi vào kho lưu trữ khác và vì vậy thường được xác thực theo cách nào đó.
Chúng tôi xem xét lệnh `git push` lần đầu tiên trong <<ch02-git-basics-chapter#_pushing_remotes>>.
Ở đây chúng tôi đề cập đến những điều cơ bản về việc đẩy một nhánh đến một kho lưu trữ từ xa.
Trong <<ch03-git-branching#_pushing_branches>>, chúng tôi đi sâu hơn một chút vào việc đẩy các nhánh cụ thể và trong <<ch03-git-branching#_tracking_branches>>, chúng tôi xem cách thiết lập các nhánh theo dõi để tự động đẩy đến.
Trong <<ch03-git-branching#_delete_branches>>, chúng tôi sử dụng cờ `--delete` để xóa một nhánh trên máy chủ bằng `git push`.
Trong suốt <<ch05-distributed-git#_contributing_project>>, chúng tôi thấy một số ví dụ về việc sử dụng `git push` để chia sẻ công việc trên các nhánh thông qua nhiều điều khiển từ xa.
Chúng tôi xem cách sử dụng nó để chia sẻ các thẻ mà bạn đã tạo với tùy chọn `--tags` trong <<ch02-git-basics-chapter#_sharing_tags>>.
Trong <<ch07-git-tools#_publishing_submodules>>, chúng tôi sử dụng tùy chọn `--recurse-submodules` để kiểm tra xem tất cả công việc mô-đun con của chúng tôi đã được xuất bản chưa trước khi đẩy siêu dự án (superproject), điều này có thể thực sự hữu ích khi sử dụng các mô-đun con.
Trong <<ch08-customizing-git#_other_client_hooks>>, chúng tôi nói ngắn gọn về hook `pre-push`, đây là một tập lệnh chúng tôi có thể thiết lập để chạy trước khi quá trình đẩy hoàn tất để xác minh rằng nó được phép đẩy.
Cuối cùng, trong <<ch10-git-internals#_pushing_refspecs>>, chúng tôi xem xét việc đẩy với một refspec đầy đủ thay vì các phím tắt chung thường được sử dụng.
Điều này có thể giúp bạn rất cụ thể về công việc bạn muốn chia sẻ.
==== git remote
Lệnh `git remote` là một công cụ quản lý cho hồ sơ của bạn về các kho lưu trữ từ xa.
Nó cho phép bạn lưu các URL dài dưới dạng các tay cầm ngắn, chẳng hạn như "`origin`" để bạn không phải nhập chúng ra mọi lúc.
Bạn có thể có một vài trong số này và lệnh `git remote` được sử dụng để thêm, thay đổi và xóa chúng.
Lệnh này được đề cập chi tiết trong <<ch02-git-basics-chapter#_remote_repos>>, bao gồm liệt kê, thêm, xóa và đổi tên chúng.
Nó cũng được sử dụng trong gần như mọi chương tiếp theo trong cuốn sách, nhưng luôn ở định dạng `git remote add <name> <url>` tiêu chuẩn.
==== git archive
Lệnh `git archive` được sử dụng để tạo tệp lưu trữ của một ảnh chụp nhanh cụ thể của dự án.
Chúng tôi sử dụng `git archive` để tạo một tarball của một dự án để chia sẻ trong <<ch05-distributed-git#_preparing_release>>.
==== git submodule
Lệnh `git submodule` được sử dụng để quản lý các kho lưu trữ bên ngoài trong một kho lưu trữ bình thường.
Điều này có thể dành cho các thư viện hoặc các loại tài nguyên được chia sẻ khác.
Lệnh `submodule` có một số lệnh phụ (`add`, `update`, `sync`, v.v.) để quản lý các tài nguyên này.
Lệnh này chỉ được đề cập và hoàn toàn được đề cập trong <<ch07-git-tools#_git_submodules>>.
=== Kiểm tra và So sánh
==== git show
Lệnh `git show` có thể hiển thị một đối tượng Git theo cách đơn giản và dễ đọc cho con người.
Thông thường, bạn sẽ sử dụng lệnh này để hiển thị thông tin về một thẻ hoặc một cam kết.
Chúng tôi sử dụng nó lần đầu tiên để hiển thị thông tin thẻ được chú thích trong <<ch02-git-basics-chapter#_annotated_tags>>.
Sau đó, chúng tôi sử dụng nó khá nhiều trong <<ch07-git-tools#_revision_selection>> để hiển thị các cam kết mà các lựa chọn sửa đổi khác nhau của chúng tôi giải quyết.
Một trong những điều thú vị hơn chúng tôi làm với `git show` là trong <<ch07-git-tools#_manual_remerge>> để trích xuất nội dung tệp cụ thể của các giai đoạn khác nhau trong quá trình xung đột hợp nhất.
==== git shortlog
Lệnh `git shortlog` được sử dụng để tóm tắt đầu ra của `git log`.
Nó sẽ lấy nhiều tùy chọn giống như lệnh `git log` sẽ làm nhưng thay vì liệt kê tất cả các cam kết, nó sẽ trình bày một bản tóm tắt các cam kết được nhóm theo tác giả.
Chúng tôi đã chỉ ra cách sử dụng nó để tạo một bảng thay đổi (changelog) đẹp trong <<ch05-distributed-git#_the_shortlog>>.
==== git describe
Lệnh `git describe` được sử dụng để lấy bất cứ thứ gì giải quyết thành một cam kết và tạo ra một chuỗi có thể đọc được và sẽ không thay đổi.
Đó là một cách để có được mô tả về một cam kết không mơ hồ như SHA-1 cam kết nhưng dễ hiểu hơn.
Chúng tôi sử dụng `git describe` trong <<ch05-distributed-git#_build_number>> và <<ch05-distributed-git#_preparing_release>> để lấy một chuỗi để đặt tên cho tệp phát hành của chúng tôi.
=== Gỡ lỗi
Git có một vài lệnh được sử dụng để giúp gỡ lỗi một vấn đề trong mã của bạn.
Điều này bao gồm từ việc tìm ra nơi một cái gì đó được giới thiệu đến việc tìm ra ai đã giới thiệu nó.
==== git bisect
Công cụ `git bisect` là một công cụ gỡ lỗi cực kỳ hữu ích được sử dụng để tìm cam kết cụ thể nào là cam kết đầu tiên giới thiệu lỗi hoặc sự cố bằng cách thực hiện tìm kiếm nhị phân tự động.
Nó được đề cập đầy đủ trong <<ch07-git-tools#_binary_search>> và chỉ được đề cập trong phần đó.
==== git blame
Lệnh `git blame` chú thích các dòng của bất kỳ tệp nào với cam kết nào là cam kết cuối cùng giới thiệu thay đổi cho từng dòng của tệp và người nào đã tạo cam kết đó.
Điều này hữu ích để tìm người để hỏi thêm thông tin về một phần cụ thể trong mã của bạn.
Nó được đề cập trong <<ch07-git-tools#_file_annotation>> và chỉ được đề cập trong phần đó.
==== git grep
Lệnh `git grep` có thể giúp bạn tìm bất kỳ chuỗi hoặc biểu thức chính quy nào trong bất kỳ tệp nào trong mã nguồn của bạn, ngay cả các phiên bản cũ hơn của dự án của bạn.
Nó được đề cập trong <<ch07-git-tools#_git_grep>> và chỉ được đề cập trong phần đó.
=== Vá lỗi
Một vài lệnh trong Git tập trung vào khái niệm suy nghĩ về các cam kết về mặt các thay đổi mà chúng giới thiệu, như thể chuỗi cam kết là một loạt các bản vá.
Các lệnh này giúp bạn quản lý các nhánh của mình theo cách này.
==== git cherry-pick
Lệnh `git cherry-pick` được sử dụng để lấy thay đổi được giới thiệu trong một cam kết Git duy nhất và cố gắng giới thiệu lại nó như một cam kết mới trên nhánh bạn đang ở.
Điều này có thể hữu ích để chỉ lấy một hoặc hai cam kết từ một nhánh riêng lẻ thay vì hợp nhất trong nhánh lấy tất cả các thay đổi.
Cherry picking được mô tả và minh họa trong <<ch05-distributed-git#_rebase_cherry_pick>>.
==== git rebase
Lệnh `git rebase` về cơ bản là một `cherry-pick` tự động.
Nó xác định một loạt các cam kết và sau đó cherry-pick chúng từng cái một theo cùng một thứ tự ở một nơi khác.
Rebasing được đề cập chi tiết trong <<ch03-git-branching#_rebasing>>, bao gồm việc đề cập đến các vấn đề cộng tác liên quan đến việc rebase các nhánh đã công khai.
Chúng tôi sử dụng nó trong thực tế trong một ví dụ về việc chia lịch sử của bạn thành hai kho lưu trữ riêng biệt trong <<ch07-git-tools#_replace>>, sử dụng cờ `--onto` cũng vậy.
Chúng tôi đi qua việc gặp xung đột hợp nhất trong quá trình rebase trong <<ch07-git-tools#ref_rerere>>.
Chúng tôi cũng sử dụng nó trong chế độ viết kịch bản tương tác với tùy chọn `-i` trong <<ch07-git-tools#_changing_multiple>>.
==== git revert
Lệnh `git revert` về cơ bản là một `git cherry-pick` ngược lại.
Nó tạo ra một cam kết mới áp dụng chính xác ngược lại thay đổi được giới thiệu trong cam kết bạn đang nhắm mục tiêu, về cơ bản là hoàn tác hoặc đảo ngược nó.
Chúng tôi sử dụng điều này trong <<ch07-git-tools#_reverse_commit>> để hoàn tác một cam kết hợp nhất.
=== Email
Nhiều dự án Git, bao gồm cả chính Git, được duy trì hoàn toàn qua danh sách gửi thư.
Git có một số công cụ được tích hợp sẵn giúp quá trình này dễ dàng hơn, từ việc tạo các bản vá bạn có thể dễ dàng gửi email đến việc áp dụng các bản vá đó từ hộp thư email.
==== git apply
Lệnh `git apply` áp dụng một bản vá được tạo bằng lệnh `git diff` hoặc thậm chí là GNU diff.
Nó tương tự như những gì lệnh `patch` có thể làm với một vài khác biệt nhỏ.
Chúng tôi minh họa việc sử dụng nó và các trường hợp bạn có thể làm như vậy trong <<ch05-distributed-git#_patches_from_email>>.
==== git am
Lệnh `git am` được sử dụng để áp dụng các bản vá từ hộp thư đến email, cụ thể là hộp thư được định dạng mbox.
Điều này hữu ích để nhận các bản vá qua email và áp dụng chúng vào dự án của bạn một cách dễ dàng.
Chúng tôi đã đề cập đến cách sử dụng và quy trình làm việc xung quanh `git am` trong <<ch05-distributed-git#_git_am>> bao gồm sử dụng các tùy chọn `--resolved`, `-i` và `-3`.
Ngoài ra còn có một số hook bạn có thể sử dụng để trợ giúp quy trình làm việc xung quanh `git am` và tất cả chúng đều được đề cập trong <<ch08-customizing-git#_email_hooks>>.
Chúng tôi cũng sử dụng nó để áp dụng các thay đổi Yêu cầu Kéo (Pull Request) GitHub được định dạng bản vá trong <<ch06-github#_email_notifications>>.
==== git format-patch
Lệnh `git format-patch` được sử dụng để tạo một loạt các bản vá ở định dạng mbox mà bạn có thể sử dụng để gửi đến danh sách gửi thư được định dạng đúng.
Chúng tôi đi qua một ví dụ về việc đóng góp cho một dự án bằng công cụ `git format-patch` trong <<ch05-distributed-git#_project_over_email>>.
==== git imap-send
Lệnh `git imap-send` tải lên một hộp thư được tạo bằng `git format-patch` vào thư mục nháp IMAP.
Chúng tôi đi qua một ví dụ về việc đóng góp cho một dự án bằng cách gửi các bản vá bằng công cụ `git imap-send` trong <<ch05-distributed-git#_project_over_email>>.
==== git send-email
Lệnh `git send-email` được sử dụng để gửi các bản vá được tạo bằng `git format-patch` qua email.
Chúng tôi đi qua một ví dụ về việc đóng góp cho một dự án bằng cách gửi các bản vá bằng công cụ `git send-email` trong <<ch05-distributed-git#_project_over_email>>.
==== git request-pull
Lệnh `git request-pull` chỉ đơn giản là được sử dụng để tạo một nội dung thư ví dụ để gửi email cho ai đó.
Nếu bạn có một nhánh trên máy chủ công cộng và muốn cho ai đó biết cách tích hợp những thay đổi đó mà không cần gửi các bản vá qua email, bạn có thể chạy lệnh này và gửi đầu ra cho người bạn muốn kéo các thay đổi vào.
Chúng tôi minh họa cách sử dụng `git request-pull` để tạo thông báo kéo trong <<ch05-distributed-git#_public_project>>.
=== Hệ thống Bên ngoài
Git đi kèm với một vài lệnh để tích hợp với các hệ thống kiểm soát phiên bản khác.
==== git svn
Lệnh `git svn` được sử dụng để giao tiếp với hệ thống kiểm soát phiên bản Subversion như một máy khách.
Điều này có nghĩa là bạn có thể sử dụng Git để kiểm xuất từ và cam kết với máy chủ Subversion.
Lệnh này được đề cập sâu trong <<ch09-git-and-other-systems#_git_svn>>.
==== git fast-import
Đối với các hệ thống kiểm soát phiên bản khác hoặc nhập từ gần như bất kỳ định dạng nào, bạn có thể sử dụng `git fast-import` để nhanh chóng ánh xạ định dạng khác sang thứ gì đó mà Git có thể dễ dàng ghi lại.
Lệnh này được đề cập sâu trong <<ch09-git-and-other-systems#_custom_importer>>.
=== Quản trị
Nếu bạn đang quản trị một kho lưu trữ Git hoặc cần sửa chữa một cái gì đó theo cách lớn, Git cung cấp một số lệnh quản trị để giúp bạn.
==== git gc
Lệnh `git gc` chạy "`garbage collection`" (thu gom rác) trên kho lưu trữ của bạn, xóa các tệp không cần thiết trong cơ sở dữ liệu của bạn và đóng gói các tệp còn lại thành một định dạng hiệu quả hơn.
Lệnh này thường chạy trong nền cho bạn, mặc dù bạn có thể chạy thủ công nếu muốn.
Chúng tôi đi qua một số ví dụ về điều này trong <<ch10-git-internals#_git_gc>>.
==== git fsck
Lệnh `git fsck` được sử dụng để kiểm tra cơ sở dữ liệu nội bộ xem có vấn đề hoặc sự không nhất quán nào không.
Chúng tôi chỉ sử dụng nhanh điều này một lần trong <<ch10-git-internals#_data_recovery>> để tìm kiếm các đối tượng lơ lửng.
==== git reflog
Lệnh `git reflog` đi qua một nhật ký về nơi tất cả các đầu của các nhánh của bạn đã ở khi bạn làm việc để tìm các cam kết bạn có thể đã mất thông qua việc viết lại lịch sử.
Chúng tôi đề cập đến lệnh này chủ yếu trong <<ch07-git-tools#_git_reflog>>, nơi chúng tôi hiển thị cách sử dụng bình thường và cách sử dụng `git log -g` để xem cùng một thông tin với đầu ra `git log`.
Chúng tôi cũng đi qua một ví dụ thực tế về việc khôi phục một nhánh bị mất như vậy trong <<ch10-git-internals#_data_recovery>>.
==== git filter-branch
Lệnh `git filter-branch` được sử dụng để viết lại vô số cam kết theo các mẫu nhất định, chẳng hạn như xóa một tệp ở mọi nơi hoặc lọc toàn bộ kho lưu trữ xuống một thư mục con duy nhất để trích xuất một dự án.
Trong <<ch07-git-tools#_removing_file_every_commit>>, chúng tôi giải thích lệnh và khám phá một số tùy chọn khác nhau như `--commit-filter`, `--subdirectory-filter` và `--tree-filter`.
Trong <<ch09-git-and-other-systems#_git_p4>>, chúng tôi sử dụng nó để sửa chữa các kho lưu trữ bên ngoài đã nhập.
=== Các Lệnh Plumbing
Cũng có khá nhiều lệnh plumbing cấp thấp hơn mà chúng tôi đã gặp trong cuốn sách.
Lệnh đầu tiên chúng tôi gặp là `ls-remote` trong <<ch06-github#_pr_refs>> mà chúng tôi sử dụng để xem các tham chiếu thô trên máy chủ.
Chúng tôi sử dụng `ls-files` trong <<ch07-git-tools#_manual_remerge>>, <<ch07-git-tools#ref_rerere>> và <<ch07-git-tools#_the_index>> để có cái nhìn thô hơn về khu vực tổ chức của bạn trông như thế nào.
Chúng tôi cũng đề cập đến `rev-parse` trong <<ch07-git-tools#_branch_references>> để lấy bất kỳ chuỗi nào và biến nó thành một đối tượng SHA-1.
Tuy nhiên, hầu hết các lệnh plumbing cấp thấp mà chúng tôi đề cập đều nằm trong <<ch10-git-internals#ch10-git-internals>>, ít nhiều là những gì chương này tập trung vào.
Chúng tôi đã cố gắng tránh sử dụng chúng trong hầu hết phần còn lại của cuốn sách.